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
35use super::freelist::FreeList;
36use super::page::{Page, PageError, PageType, PAGE_SIZE};
37use super::page_cache::PageCache;
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40use std::fs::{File, OpenOptions};
41use std::io::{Read, Seek, SeekFrom, Write};
42use std::path::{Path, PathBuf};
43#[cfg(test)]
44use std::sync::atomic::{AtomicU8, Ordering};
45use std::sync::{Arc, Mutex, RwLock};
46
47pub use reddb_file::{DatabaseHeader, PhysicalFileHeader};
48
49/// Default cache size (pages)
50const DEFAULT_CACHE_SIZE: usize = 10_000;
51const DWB_ZONE_START_PAGE: u32 = 3;
52const DWB_ZONE_PAGES: u32 = 64;
53const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
54const DWB_MAX_PAGES_PER_FRAME: usize =
55    (DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
56const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
57
58#[cfg(test)]
59static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
60
61/// Pager error types
62#[derive(Debug)]
63pub enum PagerError {
64    /// I/O error
65    Io(std::io::Error),
66    /// Page error
67    Page(PageError),
68    /// Invalid database file
69    InvalidDatabase(String),
70    /// Database is read-only
71    ReadOnly,
72    /// Page not found
73    PageNotFound(u32),
74    /// Database is locked
75    Locked,
76    /// A Mutex or RwLock was poisoned (another thread panicked while holding it)
77    LockPoisoned,
78    /// Database is encrypted but no key was supplied.
79    EncryptionRequired,
80    /// Plain (unencrypted) database opened with an encryption key.
81    PlainDatabaseRefusesKey,
82    /// Encryption key validation failed for an encrypted database.
83    InvalidKey,
84    /// Both superblock copies failed validation (ADR 0074 §2).
85    SuperblockZoneUnrecoverable(PathBuf),
86    /// The internal manifest zone failed its checksum (ADR 0074 §2).
87    ManifestZoneCorrupt(PathBuf),
88    /// The in-file double-write zone failed validation (ADR 0074 §2).
89    DwbZoneCorrupt(PathBuf),
90    /// The store still carries a retired phase-1 pager sidecar (ADR 0038 §4).
91    LegacySidecarStore { sidecar: PathBuf },
92    /// The store predates the phase-3 in-file DWB layout marker (ADR 0038 §4).
93    LegacyPagedStore { path: PathBuf },
94}
95
96/// A contiguous run of database pages reserved for vector-turbo payloads.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct ExtentId {
99    pub start_page: u32,
100    pub n_pages: u32,
101}
102
103impl std::fmt::Display for PagerError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        match self {
106            Self::Io(e) => write!(f, "I/O error: {}", e),
107            Self::Page(e) => write!(f, "Page error: {}", e),
108            Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
109            Self::ReadOnly => write!(f, "Database is read-only"),
110            Self::PageNotFound(id) => write!(f, "Page {} not found", id),
111            Self::Locked => write!(f, "Database is locked"),
112            Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
113            Self::EncryptionRequired => write!(
114                f,
115                "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
116            ),
117            Self::PlainDatabaseRefusesKey => write!(
118                f,
119                "Plain (unencrypted) database opened with an encryption key — refusing"
120            ),
121            Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
122            Self::SuperblockZoneUnrecoverable(path) => write!(
123                f,
124                "superblock zone of {} is unrecoverable: both ping-pong copies failed \
125                 validation, so no generation can be trusted to root the store. The store \
126                 will not be opened. Recover what survives with red salvage \
127                 (ADR 0074 §4); it reads the damaged file without writing to it and \
128                 reports what it could not verify.",
129                path.display()
130            ),
131            Self::ManifestZoneCorrupt(path) => write!(
132                f,
133                "internal manifest zone of {} failed its checksum: the zone that names \
134                 collections, indexes and the checkpoint boundary cannot be trusted, so no \
135                 rows are returned rather than garbage ones. Run the `SCRUB` statement to classify the fault \
136                 and `red salvage` to extract what survives (ADR 0074 §2/§4).",
137                path.display()
138            ),
139            Self::DwbZoneCorrupt(path) => write!(
140                f,
141                "double-write zone of {} failed validation: page recovery cannot prove \
142                 whether the protected in-place write completed. The store will not be opened \
143                 silently; run the `SCRUB` statement to classify the fault and `red salvage` to extract trusted \
144                 rows (ADR 0074 §2/§4).",
145                path.display()
146            ),
147            Self::LegacySidecarStore { sidecar } => write!(
148                f,
149                "refusing to open a legacy sidecar-backed store: found {}. The superblock \
150                 pair, internal manifest, and double-write recovery state now live inside the \
151                 .rdb file (ADR 0038 §2/§4), \
152                 and the engine never reads the retired sidecars silently. Convert the store \
153                 first with the offline migration tool \
154                 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
155                sidecar.display()
156            ),
157            Self::LegacyPagedStore { path } => write!(
158                f,
159                "refusing to open a pre-phase-3 paged store: {} does not carry the layout marker \
160                 for the in-file double-write zone. Convert the store first with the offline \
161                 migration tool (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is \
162                 reversible.",
163                path.display()
164            ),
165        }
166    }
167}
168
169impl std::error::Error for PagerError {
170    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
171        match self {
172            Self::Io(e) => Some(e),
173            Self::Page(e) => Some(e),
174            _ => None,
175        }
176    }
177}
178
179impl From<std::io::Error> for PagerError {
180    fn from(e: std::io::Error) -> Self {
181        Self::Io(e)
182    }
183}
184
185impl From<PageError> for PagerError {
186    fn from(e: PageError) -> Self {
187        Self::Page(e)
188    }
189}
190
191/// Pager configuration
192#[derive(Debug, Clone)]
193pub struct PagerConfig {
194    /// Page cache capacity
195    pub cache_size: usize,
196    /// Whether to open read-only
197    pub read_only: bool,
198    /// Whether to create if not exists
199    pub create: bool,
200    /// Whether to verify checksums on read
201    pub verify_checksums: bool,
202    /// Enable double-write buffer for torn page protection
203    pub double_write: bool,
204    /// Optional encryption key. When set, `Pager::open` writes/reads
205    /// pages through `PageEncryptor` and rejects any DB whose
206    /// encryption-marker disagrees with the supplied key (or its
207    /// absence). When `None`, the pager refuses to open a DB whose
208    /// header carries the `RDBE` encryption marker.
209    pub encryption: Option<crate::storage::encryption::SecureKey>,
210}
211
212impl Default for PagerConfig {
213    fn default() -> Self {
214        Self {
215            cache_size: DEFAULT_CACHE_SIZE,
216            read_only: false,
217            create: true,
218            verify_checksums: true,
219            double_write: true,
220            encryption: None,
221        }
222    }
223}
224
225/// Page I/O Manager
226///
227/// Handles reading/writing pages and manages the page cache.
228pub struct Pager {
229    /// Database file path
230    path: PathBuf,
231    /// File handle
232    file: Mutex<File>,
233    /// Exclusive file lock (held for lifetime, released on drop)
234    _lock_file: Option<File>,
235    /// Whether mutable pages are protected by the in-file DWB zone.
236    dwb_enabled: bool,
237    /// Page cache
238    cache: PageCache,
239    /// Free page list
240    freelist: RwLock<FreeList>,
241    /// Reused page ownership must reach both superblock copies before a
242    /// manifest can refer to those pages, including single-slot recovery.
243    allocation_reused: std::sync::atomic::AtomicBool,
244    /// A consumed trunk cannot be reused until both header fences succeed.
245    allocation_fence_pending: std::sync::atomic::AtomicBool,
246    /// Database header
247    header: RwLock<DatabaseHeader>,
248    /// Configuration
249    config: PagerConfig,
250    /// Dirty flag for header
251    header_dirty: Mutex<bool>,
252    /// Optional WAL writer for WAL-first flush ordering.
253    ///
254    /// When set, [`Pager::flush`] computes the maximum `header.lsn` of
255    /// every dirty page and calls [`WalWriter::flush_until`] before
256    /// passing the batch to the double-write buffer. This guarantees
257    /// the postgres-style invariant: a page on disk implies its WAL
258    /// record is already durable.
259    ///
260    /// Wired in via [`Pager::set_wal_writer`] post-construction so
261    /// existing callers that build a Pager without a WAL keep working
262    /// unchanged. See `PLAN.md` § Target 3.
263    wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
264    /// Optional page encryptor + header. When set, `read_page` /
265    /// `write_page` route through AES-GCM transparently and page 0
266    /// bypasses encryption (it carries the encryption marker +
267    /// header itself). When `None`, all pages are stored plaintext
268    /// and any DB header carrying the `RDBE` marker is rejected at
269    /// open time.
270    pub(crate) encryption: Option<(
271        crate::storage::encryption::PageEncryptor,
272        crate::storage::encryption::EncryptionHeader,
273    )>,
274}
275
276#[path = "pager/impl.rs"]
277mod pager_impl;
278impl Drop for Pager {
279    fn drop(&mut self) {
280        // Try to flush on drop
281        let _ = self.flush();
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    #[cfg(target_os = "linux")]
289    use pager_impl::parse_mountinfo_options_for_path;
290    use pager_impl::{
291        classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
292    };
293    use std::fs;
294    use std::io::Write;
295
296    fn temp_db_path() -> PathBuf {
297        use std::sync::atomic::{AtomicU64, Ordering};
298        static COUNTER: AtomicU64 = AtomicU64::new(0);
299        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
300        let mut path = std::env::temp_dir();
301        path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
302        path
303    }
304
305    fn cleanup(path: &Path) {
306        // Remove the data file AND every pager shadow sidecar via reddb-file's
307        // authoritative path helpers (the pager writes only these three shadows
308        // next to the data file). Deriving the paths through reddb-file keeps the
309        // file-suffix contract out of the server source (enforced by reddb-file's
310        // `server_source_does_not_embed_owned_file_suffixes`). Tests drop the
311        // pager BEFORE calling this (every call site uses an inner block) so the
312        // drop-time flush cannot re-create a sidecar after removal.
313        let _ = fs::remove_file(path);
314        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
315            let _ = fs::remove_file(sidecar);
316        }
317    }
318
319    fn dwb_path_for(path: &Path) -> PathBuf {
320        reddb_file::layout::pager_dwb_shadow_path(path)
321    }
322
323    fn dwb_zone_offset_for_test() -> u64 {
324        3 * PAGE_SIZE as u64
325    }
326
327    static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
328
329    struct CowAtomicWriteOverrideGuard {
330        _guard: std::sync::MutexGuard<'static, ()>,
331    }
332
333    impl Drop for CowAtomicWriteOverrideGuard {
334        fn drop(&mut self) {
335            COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
336        }
337    }
338
339    fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
340        let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
341            .lock()
342            .unwrap_or_else(|err| err.into_inner());
343        COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
344        CowAtomicWriteOverrideGuard { _guard: guard }
345    }
346
347    fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
348        let pages: Vec<_> = pages
349            .iter()
350            .map(|(page_id, page)| {
351                let mut page = page.clone();
352                page.update_checksum();
353                (*page_id, page)
354            })
355            .collect();
356        let buf = reddb_file::encode_paged_dwb_frame(
357            pages
358                .iter()
359                .map(|(page_id, page)| (*page_id, page.as_bytes())),
360        );
361
362        let mut file = OpenOptions::new()
363            .read(true)
364            .write(true)
365            .open(path)
366            .expect("open() should succeed");
367        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
368            .expect("seek() should succeed");
369        file.write_all(&buf).expect("write_all() should succeed");
370        file.sync_all().expect("sync_all() should succeed");
371    }
372
373    fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
374        let mut file = OpenOptions::new()
375            .write(true)
376            .open(path)
377            .expect("open() should succeed");
378        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
379            .expect("value is present");
380        file.write_all(page.as_bytes())
381            .expect("write_all() should succeed");
382        file.sync_all().expect("sync_all() should succeed");
383    }
384
385    fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
386        let mut torn = *before.as_bytes();
387        torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
388
389        let mut file = OpenOptions::new()
390            .write(true)
391            .open(path)
392            .expect("open() should succeed");
393        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
394            .expect("value is present");
395        file.write_all(&torn).expect("write_all() should succeed");
396        file.sync_all().expect("sync_all() should succeed");
397    }
398
399    #[test]
400    fn test_pager_create_new() {
401        let path = temp_db_path();
402        cleanup(&path);
403
404        {
405            let pager = Pager::open_default(&path).expect("open_default() should succeed");
406            assert_eq!(
407                pager.page_count().expect("page_count() should succeed"),
408                FIRST_ALLOCATABLE_PAGE
409            );
410            // Header + reserved metadata pages + DWB zone
411        }
412
413        cleanup(&path);
414    }
415
416    #[test]
417    fn test_pager_reopen() {
418        let path = temp_db_path();
419        cleanup(&path);
420
421        // Create and write
422        {
423            let pager = Pager::open_default(&path).expect("open_default() should succeed");
424
425            // Allocate a page
426            let page = pager
427                .allocate_page(PageType::BTreeLeaf)
428                .expect("allocate_page() should succeed");
429            assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
430
431            pager.sync().expect("sync() should succeed");
432        }
433
434        // Reopen and verify
435        {
436            let pager = Pager::open_default(&path).expect("open_default() should succeed");
437            assert_eq!(
438                pager.page_count().expect("page_count() should succeed"),
439                FIRST_ALLOCATABLE_PAGE + 1
440            );
441            // Header + reserved metadata pages + DWB zone + 1 data page
442        }
443
444        cleanup(&path);
445    }
446
447    #[test]
448    fn test_pager_read_write() {
449        let path = temp_db_path();
450        cleanup(&path);
451
452        {
453            let pager = Pager::open_default(&path).expect("open_default() should succeed");
454
455            // Allocate and write
456            let mut page = pager
457                .allocate_page(PageType::BTreeLeaf)
458                .expect("allocate_page() should succeed");
459            let page_id = page.page_id();
460
461            page.insert_cell(b"key", b"value")
462                .expect("insert_cell() should succeed");
463            pager
464                .write_page(page_id, page)
465                .expect("write_page() should succeed");
466
467            // Read back
468            let read_page = pager
469                .read_page(page_id)
470                .expect("read_page() should succeed");
471            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
472            assert_eq!(key, b"key");
473            assert_eq!(value, b"value");
474        }
475
476        cleanup(&path);
477    }
478
479    #[test]
480    fn test_pager_cache() {
481        let path = temp_db_path();
482        cleanup(&path);
483
484        {
485            let pager = Pager::open_default(&path).expect("open_default() should succeed");
486
487            // Allocate a page
488            let page = pager
489                .allocate_page(PageType::BTreeLeaf)
490                .expect("allocate_page() should succeed");
491            let page_id = page.page_id();
492
493            // First read - should be cached from allocate
494            let _ = pager
495                .read_page(page_id)
496                .expect("read_page() should succeed");
497
498            // Second read - should hit cache
499            let _ = pager
500                .read_page(page_id)
501                .expect("read_page() should succeed");
502
503            let stats = pager.cache_stats();
504            assert!(stats.hits >= 1);
505        }
506
507        cleanup(&path);
508    }
509
510    #[test]
511    fn test_pager_free_page() {
512        let path = temp_db_path();
513        cleanup(&path);
514
515        {
516            let pager = Pager::open_default(&path).expect("open_default() should succeed");
517
518            // Allocate pages
519            let page1 = pager
520                .allocate_page(PageType::BTreeLeaf)
521                .expect("allocate_page() should succeed");
522            let page2 = pager
523                .allocate_page(PageType::BTreeLeaf)
524                .expect("allocate_page() should succeed");
525
526            let id1 = page1.page_id();
527            let id2 = page2.page_id();
528
529            // Free page 1
530            pager.free_page(id1).expect("free_page() should succeed");
531
532            // Next allocation should reuse page 1
533            let page3 = pager
534                .allocate_page(PageType::BTreeLeaf)
535                .expect("allocate_page() should succeed");
536            assert_eq!(page3.page_id(), id1);
537        }
538
539        cleanup(&path);
540    }
541
542    #[test]
543    fn test_freelist_persistence() {
544        let path = temp_db_path();
545        cleanup(&path);
546
547        let freed_id;
548        {
549            let pager = Pager::open_default(&path).expect("open_default() should succeed");
550            let page1 = pager
551                .allocate_page(PageType::BTreeLeaf)
552                .expect("allocate_page() should succeed");
553            let _page2 = pager
554                .allocate_page(PageType::BTreeLeaf)
555                .expect("allocate_page() should succeed");
556            freed_id = page1.page_id();
557
558            pager
559                .free_page(freed_id)
560                .expect("free_page() should succeed");
561            pager.sync().expect("sync() should succeed");
562        }
563
564        {
565            let pager = Pager::open_default(&path).expect("open_default() should succeed");
566            let page = pager
567                .allocate_page(PageType::BTreeLeaf)
568                .expect("allocate_page() should succeed");
569            assert_eq!(page.page_id(), freed_id);
570        }
571
572        cleanup(&path);
573    }
574
575    #[test]
576    fn test_pager_read_only() {
577        let path = temp_db_path();
578        cleanup(&path);
579
580        // Create database
581        {
582            let pager = Pager::open_default(&path).expect("open_default() should succeed");
583            pager.sync().expect("sync() should succeed");
584        }
585
586        // Open read-only
587        {
588            let config = PagerConfig {
589                read_only: true,
590                ..Default::default()
591            };
592
593            let pager = Pager::open(&path, config).expect("open() should succeed");
594            assert!(pager.is_read_only());
595
596            // Should fail to allocate
597            assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
598        }
599
600        cleanup(&path);
601    }
602
603    #[test]
604    fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
605        let path = temp_db_path();
606        cleanup(&path);
607
608        let config = PagerConfig {
609            double_write: true,
610            ..Default::default()
611        };
612
613        let page_id;
614        {
615            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
616            let page = pager
617                .allocate_page(PageType::BTreeLeaf)
618                .expect("allocate_page() should succeed");
619            page_id = page.page_id();
620            pager.sync().expect("sync() should succeed");
621        }
622
623        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
624        recovered_page
625            .insert_cell(b"key", b"value")
626            .expect("insert_cell() should succeed");
627        write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
628
629        assert!(!dwb_path_for(&path).exists());
630
631        {
632            let pager = Pager::open(&path, config).expect("open() should succeed");
633
634            let read_page = pager
635                .read_page(page_id)
636                .expect("read_page() should succeed");
637            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
638            assert_eq!(key, b"key");
639            assert_eq!(value, b"value");
640
641            assert!(!dwb_path_for(&path).exists());
642
643            let mut updated_page = recovered_page.clone();
644            updated_page
645                .insert_cell(b"key2", b"value2")
646                .expect("insert_cell() should succeed");
647            pager
648                .write_page(page_id, updated_page)
649                .expect("write_page() should succeed");
650            pager.flush().expect("flush() should succeed");
651
652            assert!(!dwb_path_for(&path).exists());
653        }
654
655        cleanup(&path);
656    }
657
658    #[test]
659    fn retired_dwb_sidecar_routes_through_offline_migration() {
660        let path = temp_db_path();
661        cleanup(&path);
662
663        {
664            let pager = Pager::open_default(&path).expect("open_default() should succeed");
665            pager.sync().expect("sync() should succeed");
666        }
667
668        let dwb = dwb_path_for(&path);
669        fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
670
671        let err = match Pager::open_default(&path) {
672            Ok(_) => panic!("retired DWB sidecar must refuse open"),
673            Err(err) => err,
674        };
675        assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
676
677        cleanup(&path);
678    }
679
680    #[test]
681    fn interrupted_dwb_staging_write_is_discarded_on_open() {
682        let path = temp_db_path();
683        cleanup(&path);
684
685        {
686            let pager = Pager::open_default(&path).expect("open_default() should succeed");
687            pager.sync().expect("sync() should succeed");
688        }
689
690        let mut file = OpenOptions::new()
691            .read(true)
692            .write(true)
693            .open(&path)
694            .expect("open() should succeed");
695        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
696            .expect("seek() should succeed");
697        file.write_all(b"RDDW")
698            .expect("write corrupt DWB magic should succeed");
699        file.sync_all().expect("sync_all() should succeed");
700
701        {
702            let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
703            pager.sync().expect("sync() should succeed");
704        }
705
706        let mut cleared = [0u8; 4];
707        let mut file = OpenOptions::new()
708            .read(true)
709            .open(&path)
710            .expect("open() should succeed");
711        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
712            .expect("seek() should succeed");
713        file.read_exact(&mut cleared)
714            .expect("read_exact() should succeed");
715        assert_ne!(cleared, reddb_file::DWB_MAGIC);
716
717        cleanup(&path);
718    }
719
720    #[test]
721    fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
722        assert_eq!(
723            classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
724            Some(CowFilesystemKind::Zfs),
725            "ZFS is always CoW"
726        );
727        assert_eq!(
728            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
729            Some(CowFilesystemKind::BtrfsDataCow),
730            "btrfs qualifies only when datacow remains enabled"
731        );
732        assert_eq!(
733            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
734            None,
735            "btrfs nodatacow mount option must reject DWB skip"
736        );
737        assert_eq!(
738            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
739            None,
740            "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
741        );
742        assert_eq!(
743            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
744            None,
745            "missing btrfs inode flags are uncertain and must fail closed"
746        );
747        assert_eq!(
748            classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
749            None,
750            "missing btrfs mount options are uncertain and must fail closed"
751        );
752    }
753
754    #[cfg(target_os = "linux")]
755    #[test]
756    fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
757        let mountinfo = "\
75824 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
75935 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
76036 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
761";
762
763        assert_eq!(
764            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
765                .as_deref(),
766            Some("rw,relatime,rw,space_cache=v2")
767        );
768        assert_eq!(
769            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
770                .as_deref(),
771            Some("rw,relatime,rw,nodatacow")
772        );
773    }
774
775    #[test]
776    fn double_write_false_keeps_dwb_when_cow_probe_denies() {
777        let _override = cow_atomic_write_override(false);
778        let path = temp_db_path();
779        cleanup(&path);
780
781        {
782            let config = PagerConfig {
783                double_write: false,
784                ..Default::default()
785            };
786            let pager = Pager::open(&path, config).expect("open() should succeed");
787            let page = pager
788                .allocate_page(PageType::BTreeLeaf)
789                .expect("allocate_page() should succeed");
790            pager
791                .write_page(page.page_id(), page)
792                .expect("write_page() should succeed");
793            pager.flush().expect("flush() should succeed");
794        }
795
796        assert!(
797            !dwb_path_for(&path).exists(),
798            "DWB must stay enabled in-file when double_write=false is not proven safe"
799        );
800
801        cleanup(&path);
802    }
803
804    #[test]
805    fn double_write_false_skips_dwb_when_cow_probe_allows() {
806        let _override = cow_atomic_write_override(true);
807        let path = temp_db_path();
808        cleanup(&path);
809
810        {
811            let config = PagerConfig {
812                double_write: false,
813                ..Default::default()
814            };
815            let pager = Pager::open(&path, config).expect("open() should succeed");
816            let page = pager
817                .allocate_page(PageType::BTreeLeaf)
818                .expect("allocate_page() should succeed");
819            pager
820                .write_page(page.page_id(), page)
821                .expect("write_page() should succeed");
822            pager.flush().expect("flush() should succeed");
823        }
824
825        assert!(
826            !dwb_path_for(&path).exists(),
827            "DWB may be skipped only after the CoW probe allows it"
828        );
829
830        cleanup(&path);
831    }
832
833    #[test]
834    fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
835        let _override = cow_atomic_write_override(true);
836        let path = temp_db_path();
837        cleanup(&path);
838
839        let page_id;
840        {
841            let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
842            let page = pager
843                .allocate_page(PageType::BTreeLeaf)
844                .expect("allocate_page() should succeed");
845            page_id = page.page_id();
846            pager.sync().expect("sync() should succeed");
847        }
848
849        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
850        recovered_page
851            .insert_cell(b"key", b"value")
852            .expect("insert_cell() should succeed");
853        write_dwb_fixture(&path, &[(page_id, recovered_page)]);
854
855        {
856            let config = PagerConfig {
857                double_write: false,
858                ..Default::default()
859            };
860            let pager = Pager::open(&path, config).expect("open() should succeed");
861            let read_page = pager
862                .read_page(page_id)
863                .expect("read_page() should succeed");
864            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
865            assert_eq!(key, b"key");
866            assert_eq!(value, b"value");
867        }
868
869        assert!(!dwb_path_for(&path).exists());
870
871        cleanup(&path);
872    }
873
874    #[test]
875    fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
876        let _override = cow_atomic_write_override(true);
877        let path = temp_db_path();
878        cleanup(&path);
879
880        let config = PagerConfig {
881            double_write: false,
882            ..Default::default()
883        };
884
885        let page_id;
886        let before;
887        let after;
888        {
889            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
890            let mut page = pager
891                .allocate_page(PageType::BTreeLeaf)
892                .expect("allocate_page() should succeed");
893            page_id = page.page_id();
894            page.insert_cell(b"phase", b"before")
895                .expect("insert_cell() should succeed");
896            pager
897                .write_page(page_id, page)
898                .expect("write_page() should succeed");
899            pager.sync().expect("sync() should succeed");
900            before = pager
901                .read_page(page_id)
902                .expect("read_page() should succeed");
903
904            let mut page = before.clone();
905            page.insert_cell(b"phase2", b"after")
906                .expect("insert_cell() should succeed");
907            pager
908                .write_page(page_id, page)
909                .expect("write_page() should succeed");
910            pager.flush().expect("flush() should succeed");
911            after = pager
912                .read_page(page_id)
913                .expect("read_page() should succeed");
914        }
915
916        // CoW crash model: the interrupted write leaves either the old full
917        // page or the new full page, never a torn mix. Exercise both outcomes.
918        for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
919            write_page_bytes(&path, page_id, whole_page);
920
921            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
922            let recovered = pager
923                .read_page(page_id)
924                .expect("read_page() should succeed");
925            assert_eq!(recovered.cell_count(), expected_cells);
926            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
927            assert_eq!(key, b"phase");
928            assert_eq!(value, b"before");
929            if expected_cells == 2 {
930                let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
931                assert_eq!(key, b"phase2");
932                assert_eq!(value, b"after");
933            }
934            drop(pager);
935        }
936
937        cleanup(&path);
938    }
939
940    #[test]
941    fn same_mid_write_without_cow_recovers_from_dwb() {
942        let _override = cow_atomic_write_override(false);
943        let path = temp_db_path();
944        cleanup(&path);
945
946        let config = PagerConfig {
947            double_write: false,
948            ..Default::default()
949        };
950
951        let page_id;
952        let before;
953        let after;
954        {
955            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
956            let mut page = pager
957                .allocate_page(PageType::BTreeLeaf)
958                .expect("allocate_page() should succeed");
959            page_id = page.page_id();
960            page.insert_cell(b"phase", b"before")
961                .expect("insert_cell() should succeed");
962            pager
963                .write_page(page_id, page)
964                .expect("write_page() should succeed");
965            pager.sync().expect("sync() should succeed");
966            before = pager
967                .read_page(page_id)
968                .expect("read_page() should succeed");
969
970            let mut page = before.clone();
971            page.insert_cell(b"phase2", b"after")
972                .expect("insert_cell() should succeed");
973            pager
974                .write_page(page_id, page)
975                .expect("write_page() should succeed");
976            pager.flush().expect("flush() should succeed");
977            after = pager
978                .read_page(page_id)
979                .expect("read_page() should succeed");
980        }
981
982        write_dwb_fixture(&path, &[(page_id, after.clone())]);
983        write_torn_page_bytes(&path, page_id, &before, &after);
984
985        {
986            let pager = Pager::open(&path, config).expect("open() should succeed");
987            let recovered = pager
988                .read_page(page_id)
989                .expect("read_page() should succeed");
990            assert_eq!(recovered.cell_count(), 2);
991
992            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
993            assert_eq!(key, b"phase");
994            assert_eq!(value, b"before");
995
996            let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
997            assert_eq!(key, b"phase2");
998            assert_eq!(value, b"after");
999        }
1000
1001        assert!(!dwb_path_for(&path).exists());
1002        cleanup(&path);
1003    }
1004
1005    // -----------------------------------------------------------------
1006    // Target 3: WAL-first flush ordering
1007    // -----------------------------------------------------------------
1008
1009    #[test]
1010    fn pager_starts_without_wal_writer() {
1011        let path = temp_db_path();
1012        let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1013        assert!(!pager.has_wal_writer());
1014        drop(pager);
1015        cleanup(&path);
1016    }
1017
1018    #[test]
1019    fn set_wal_writer_attaches_handle() {
1020        use crate::storage::wal::writer::WalWriter;
1021        use std::sync::{Arc, Mutex};
1022
1023        let db_path = temp_db_path();
1024        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1025        let _ = fs::remove_file(&wal_path);
1026
1027        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1028        let wal = Arc::new(Mutex::new(
1029            WalWriter::open(&wal_path).expect("open() should succeed"),
1030        ));
1031        pager.set_wal_writer(Arc::clone(&wal));
1032        assert!(pager.has_wal_writer());
1033
1034        pager.clear_wal_writer();
1035        assert!(!pager.has_wal_writer());
1036
1037        drop(pager);
1038        let _ = fs::remove_file(&wal_path);
1039        cleanup(&db_path);
1040    }
1041
1042    #[test]
1043    fn flush_with_lsn_zero_pages_skips_wal_call() {
1044        // When every dirty page has lsn == 0 (the legacy auto-commit
1045        // path), flush() must NOT call wal.flush_until — there is no
1046        // WAL record to wait for. We verify this by attaching a WAL
1047        // whose durable_lsn starts at 8 and confirming flush() does
1048        // not advance it (no append, no flush).
1049        use crate::storage::wal::writer::WalWriter;
1050        use std::sync::{Arc, Mutex};
1051
1052        let db_path = temp_db_path();
1053        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1054        let _ = fs::remove_file(&wal_path);
1055
1056        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1057        let wal = Arc::new(Mutex::new(
1058            WalWriter::open(&wal_path).expect("open() should succeed"),
1059        ));
1060        let initial_durable = {
1061            let g = wal.lock().expect("lock() should succeed");
1062            g.durable_lsn()
1063        };
1064        pager.set_wal_writer(Arc::clone(&wal));
1065
1066        // Allocate and write a page with lsn = 0.
1067        let mut page = pager
1068            .allocate_page(PageType::BTreeLeaf)
1069            .expect("allocate_page() should succeed");
1070        page.insert_cell(b"k", b"v")
1071            .expect("insert_cell() should succeed");
1072        // header.lsn stays at 0 — caller did not stamp.
1073        pager
1074            .write_page(page.page_id(), page)
1075            .expect("write_page() should succeed");
1076        pager.flush().expect("flush() should succeed");
1077
1078        // WAL durable_lsn must be unchanged because flush_until was
1079        // never called (max lsn over dirty pages was 0).
1080        let after_flush = {
1081            let g = wal.lock().expect("lock() should succeed");
1082            g.durable_lsn()
1083        };
1084        assert_eq!(after_flush, initial_durable);
1085
1086        drop(pager);
1087        let _ = fs::remove_file(&wal_path);
1088        cleanup(&db_path);
1089    }
1090
1091    #[test]
1092    fn flush_advances_wal_durable_when_pages_carry_lsn() {
1093        // The full WAL-first dance: append a record, capture the
1094        // returned LSN, stamp it on a page, flush — afterwards the
1095        // WAL must be durable up to at least that LSN.
1096        use crate::storage::wal::record::WalRecord;
1097        use crate::storage::wal::writer::WalWriter;
1098        use std::sync::{Arc, Mutex};
1099
1100        let db_path = temp_db_path();
1101        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1102        let _ = fs::remove_file(&wal_path);
1103
1104        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1105        let wal = Arc::new(Mutex::new(
1106            WalWriter::open(&wal_path).expect("open() should succeed"),
1107        ));
1108        pager.set_wal_writer(Arc::clone(&wal));
1109
1110        // Stamp two dirty pages with a real WAL LSN.
1111        let stamped_lsn = {
1112            let mut wal_guard = wal.lock().expect("lock() should succeed");
1113            wal_guard
1114                .append(&WalRecord::Begin { tx_id: 1 })
1115                .expect("append() should succeed");
1116            wal_guard
1117                .append(&WalRecord::Commit { tx_id: 1 })
1118                .expect("append() should succeed");
1119            wal_guard.current_lsn()
1120        };
1121        let mut page = pager
1122            .allocate_page(PageType::BTreeLeaf)
1123            .expect("allocate_page() should succeed");
1124        page.insert_cell(b"k", b"v")
1125            .expect("insert_cell() should succeed");
1126        // Use the public Page API to set the LSN.
1127        page.set_lsn(stamped_lsn);
1128        pager
1129            .write_page(page.page_id(), page)
1130            .expect("write_page() should succeed");
1131        pager.flush().expect("flush() should succeed");
1132
1133        // After flush, the WAL is durable at least up to our stamp.
1134        let after_flush = {
1135            let g = wal.lock().expect("lock() should succeed");
1136            g.durable_lsn()
1137        };
1138        assert!(
1139            after_flush >= stamped_lsn,
1140            "after flush durable_lsn {} must be >= stamped {}",
1141            after_flush,
1142            stamped_lsn
1143        );
1144
1145        drop(pager);
1146        let _ = fs::remove_file(&wal_path);
1147        cleanup(&db_path);
1148    }
1149
1150    // -----------------------------------------------------------------
1151    // gh-892: filesystem block-size alignment diagnostic
1152    // -----------------------------------------------------------------
1153
1154    #[test]
1155    fn block_size_warn_fires_for_mismatched_block_size() {
1156        // A block size that does not divide the 16 KiB page size means a
1157        // page write straddles FS blocks — the predicate must report a
1158        // misalignment so `open()` emits the warning.
1159        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1160        // Block larger than the page (e.g. 1 MiB): 16384 % 1048576 != 0.
1161        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1162        // 6 KiB also fails to divide 16 KiB.
1163        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1164    }
1165
1166    #[test]
1167    fn block_size_silent_for_divisor() {
1168        // Block sizes that evenly divide the page size: no straddle, no warn.
1169        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1170        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1171        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1172        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1173    }
1174
1175    #[test]
1176    fn block_size_unavailable_is_silent() {
1177        // st_blksize == 0 means the probe is unavailable; never warn on it.
1178        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1179    }
1180
1181    #[test]
1182    fn page_size_is_unchanged_16kib() {
1183        // The diagnostic must never alter the compile-time page size.
1184        assert_eq!(PAGE_SIZE, 16 * 1024);
1185    }
1186}