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 scrub 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 scrub 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    /// Database header
242    header: RwLock<DatabaseHeader>,
243    /// Configuration
244    config: PagerConfig,
245    /// Dirty flag for header
246    header_dirty: Mutex<bool>,
247    /// Optional WAL writer for WAL-first flush ordering.
248    ///
249    /// When set, [`Pager::flush`] computes the maximum `header.lsn` of
250    /// every dirty page and calls [`WalWriter::flush_until`] before
251    /// passing the batch to the double-write buffer. This guarantees
252    /// the postgres-style invariant: a page on disk implies its WAL
253    /// record is already durable.
254    ///
255    /// Wired in via [`Pager::set_wal_writer`] post-construction so
256    /// existing callers that build a Pager without a WAL keep working
257    /// unchanged. See `PLAN.md` § Target 3.
258    wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
259    /// Optional page encryptor + header. When set, `read_page` /
260    /// `write_page` route through AES-GCM transparently and page 0
261    /// bypasses encryption (it carries the encryption marker +
262    /// header itself). When `None`, all pages are stored plaintext
263    /// and any DB header carrying the `RDBE` marker is rejected at
264    /// open time.
265    pub(crate) encryption: Option<(
266        crate::storage::encryption::PageEncryptor,
267        crate::storage::encryption::EncryptionHeader,
268    )>,
269}
270
271#[path = "pager/impl.rs"]
272mod pager_impl;
273impl Drop for Pager {
274    fn drop(&mut self) {
275        // Try to flush on drop
276        let _ = self.flush();
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    #[cfg(target_os = "linux")]
284    use pager_impl::parse_mountinfo_options_for_path;
285    use pager_impl::{
286        classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
287    };
288    use std::fs;
289    use std::io::Write;
290
291    fn temp_db_path() -> PathBuf {
292        use std::sync::atomic::{AtomicU64, Ordering};
293        static COUNTER: AtomicU64 = AtomicU64::new(0);
294        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
295        let mut path = std::env::temp_dir();
296        path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
297        path
298    }
299
300    fn cleanup(path: &Path) {
301        // Remove the data file AND every pager shadow sidecar via reddb-file's
302        // authoritative path helpers (the pager writes only these three shadows
303        // next to the data file). Deriving the paths through reddb-file keeps the
304        // file-suffix contract out of the server source (enforced by reddb-file's
305        // `server_source_does_not_embed_owned_file_suffixes`). Tests drop the
306        // pager BEFORE calling this (every call site uses an inner block) so the
307        // drop-time flush cannot re-create a sidecar after removal.
308        let _ = fs::remove_file(path);
309        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
310            let _ = fs::remove_file(sidecar);
311        }
312    }
313
314    fn dwb_path_for(path: &Path) -> PathBuf {
315        reddb_file::layout::pager_dwb_shadow_path(path)
316    }
317
318    fn dwb_zone_offset_for_test() -> u64 {
319        3 * PAGE_SIZE as u64
320    }
321
322    static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
323
324    struct CowAtomicWriteOverrideGuard {
325        _guard: std::sync::MutexGuard<'static, ()>,
326    }
327
328    impl Drop for CowAtomicWriteOverrideGuard {
329        fn drop(&mut self) {
330            COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
331        }
332    }
333
334    fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
335        let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
336            .lock()
337            .unwrap_or_else(|err| err.into_inner());
338        COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
339        CowAtomicWriteOverrideGuard { _guard: guard }
340    }
341
342    fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
343        let pages: Vec<_> = pages
344            .iter()
345            .map(|(page_id, page)| {
346                let mut page = page.clone();
347                page.update_checksum();
348                (*page_id, page)
349            })
350            .collect();
351        let buf = reddb_file::encode_paged_dwb_frame(
352            pages
353                .iter()
354                .map(|(page_id, page)| (*page_id, page.as_bytes())),
355        );
356
357        let mut file = OpenOptions::new()
358            .read(true)
359            .write(true)
360            .open(path)
361            .expect("open() should succeed");
362        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
363            .expect("seek() should succeed");
364        file.write_all(&buf).expect("write_all() should succeed");
365        file.sync_all().expect("sync_all() should succeed");
366    }
367
368    fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
369        let mut file = OpenOptions::new()
370            .write(true)
371            .open(path)
372            .expect("open() should succeed");
373        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
374            .expect("value is present");
375        file.write_all(page.as_bytes())
376            .expect("write_all() should succeed");
377        file.sync_all().expect("sync_all() should succeed");
378    }
379
380    fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
381        let mut torn = *before.as_bytes();
382        torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
383
384        let mut file = OpenOptions::new()
385            .write(true)
386            .open(path)
387            .expect("open() should succeed");
388        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
389            .expect("value is present");
390        file.write_all(&torn).expect("write_all() should succeed");
391        file.sync_all().expect("sync_all() should succeed");
392    }
393
394    #[test]
395    fn test_pager_create_new() {
396        let path = temp_db_path();
397        cleanup(&path);
398
399        {
400            let pager = Pager::open_default(&path).expect("open_default() should succeed");
401            assert_eq!(
402                pager.page_count().expect("page_count() should succeed"),
403                FIRST_ALLOCATABLE_PAGE
404            );
405            // Header + reserved metadata pages + DWB zone
406        }
407
408        cleanup(&path);
409    }
410
411    #[test]
412    fn test_pager_reopen() {
413        let path = temp_db_path();
414        cleanup(&path);
415
416        // Create and write
417        {
418            let pager = Pager::open_default(&path).expect("open_default() should succeed");
419
420            // Allocate a page
421            let page = pager
422                .allocate_page(PageType::BTreeLeaf)
423                .expect("allocate_page() should succeed");
424            assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
425
426            pager.sync().expect("sync() should succeed");
427        }
428
429        // Reopen and verify
430        {
431            let pager = Pager::open_default(&path).expect("open_default() should succeed");
432            assert_eq!(
433                pager.page_count().expect("page_count() should succeed"),
434                FIRST_ALLOCATABLE_PAGE + 1
435            );
436            // Header + reserved metadata pages + DWB zone + 1 data page
437        }
438
439        cleanup(&path);
440    }
441
442    #[test]
443    fn test_pager_read_write() {
444        let path = temp_db_path();
445        cleanup(&path);
446
447        {
448            let pager = Pager::open_default(&path).expect("open_default() should succeed");
449
450            // Allocate and write
451            let mut page = pager
452                .allocate_page(PageType::BTreeLeaf)
453                .expect("allocate_page() should succeed");
454            let page_id = page.page_id();
455
456            page.insert_cell(b"key", b"value")
457                .expect("insert_cell() should succeed");
458            pager
459                .write_page(page_id, page)
460                .expect("write_page() should succeed");
461
462            // Read back
463            let read_page = pager
464                .read_page(page_id)
465                .expect("read_page() should succeed");
466            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
467            assert_eq!(key, b"key");
468            assert_eq!(value, b"value");
469        }
470
471        cleanup(&path);
472    }
473
474    #[test]
475    fn test_pager_cache() {
476        let path = temp_db_path();
477        cleanup(&path);
478
479        {
480            let pager = Pager::open_default(&path).expect("open_default() should succeed");
481
482            // Allocate a page
483            let page = pager
484                .allocate_page(PageType::BTreeLeaf)
485                .expect("allocate_page() should succeed");
486            let page_id = page.page_id();
487
488            // First read - should be cached from allocate
489            let _ = pager
490                .read_page(page_id)
491                .expect("read_page() should succeed");
492
493            // Second read - should hit cache
494            let _ = pager
495                .read_page(page_id)
496                .expect("read_page() should succeed");
497
498            let stats = pager.cache_stats();
499            assert!(stats.hits >= 1);
500        }
501
502        cleanup(&path);
503    }
504
505    #[test]
506    fn test_pager_free_page() {
507        let path = temp_db_path();
508        cleanup(&path);
509
510        {
511            let pager = Pager::open_default(&path).expect("open_default() should succeed");
512
513            // Allocate pages
514            let page1 = pager
515                .allocate_page(PageType::BTreeLeaf)
516                .expect("allocate_page() should succeed");
517            let page2 = pager
518                .allocate_page(PageType::BTreeLeaf)
519                .expect("allocate_page() should succeed");
520
521            let id1 = page1.page_id();
522            let id2 = page2.page_id();
523
524            // Free page 1
525            pager.free_page(id1).expect("free_page() should succeed");
526
527            // Next allocation should reuse page 1
528            let page3 = pager
529                .allocate_page(PageType::BTreeLeaf)
530                .expect("allocate_page() should succeed");
531            assert_eq!(page3.page_id(), id1);
532        }
533
534        cleanup(&path);
535    }
536
537    #[test]
538    fn test_freelist_persistence() {
539        let path = temp_db_path();
540        cleanup(&path);
541
542        let freed_id;
543        {
544            let pager = Pager::open_default(&path).expect("open_default() should succeed");
545            let page1 = pager
546                .allocate_page(PageType::BTreeLeaf)
547                .expect("allocate_page() should succeed");
548            let _page2 = pager
549                .allocate_page(PageType::BTreeLeaf)
550                .expect("allocate_page() should succeed");
551            freed_id = page1.page_id();
552
553            pager
554                .free_page(freed_id)
555                .expect("free_page() should succeed");
556            pager.sync().expect("sync() should succeed");
557        }
558
559        {
560            let pager = Pager::open_default(&path).expect("open_default() should succeed");
561            let page = pager
562                .allocate_page(PageType::BTreeLeaf)
563                .expect("allocate_page() should succeed");
564            assert_eq!(page.page_id(), freed_id);
565        }
566
567        cleanup(&path);
568    }
569
570    #[test]
571    fn test_pager_read_only() {
572        let path = temp_db_path();
573        cleanup(&path);
574
575        // Create database
576        {
577            let pager = Pager::open_default(&path).expect("open_default() should succeed");
578            pager.sync().expect("sync() should succeed");
579        }
580
581        // Open read-only
582        {
583            let config = PagerConfig {
584                read_only: true,
585                ..Default::default()
586            };
587
588            let pager = Pager::open(&path, config).expect("open() should succeed");
589            assert!(pager.is_read_only());
590
591            // Should fail to allocate
592            assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
593        }
594
595        cleanup(&path);
596    }
597
598    #[test]
599    fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
600        let path = temp_db_path();
601        cleanup(&path);
602
603        let config = PagerConfig {
604            double_write: true,
605            ..Default::default()
606        };
607
608        let page_id;
609        {
610            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
611            let page = pager
612                .allocate_page(PageType::BTreeLeaf)
613                .expect("allocate_page() should succeed");
614            page_id = page.page_id();
615            pager.sync().expect("sync() should succeed");
616        }
617
618        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
619        recovered_page
620            .insert_cell(b"key", b"value")
621            .expect("insert_cell() should succeed");
622        write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
623
624        assert!(!dwb_path_for(&path).exists());
625
626        {
627            let pager = Pager::open(&path, config).expect("open() should succeed");
628
629            let read_page = pager
630                .read_page(page_id)
631                .expect("read_page() should succeed");
632            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
633            assert_eq!(key, b"key");
634            assert_eq!(value, b"value");
635
636            assert!(!dwb_path_for(&path).exists());
637
638            let mut updated_page = recovered_page.clone();
639            updated_page
640                .insert_cell(b"key2", b"value2")
641                .expect("insert_cell() should succeed");
642            pager
643                .write_page(page_id, updated_page)
644                .expect("write_page() should succeed");
645            pager.flush().expect("flush() should succeed");
646
647            assert!(!dwb_path_for(&path).exists());
648        }
649
650        cleanup(&path);
651    }
652
653    #[test]
654    fn retired_dwb_sidecar_routes_through_offline_migration() {
655        let path = temp_db_path();
656        cleanup(&path);
657
658        {
659            let pager = Pager::open_default(&path).expect("open_default() should succeed");
660            pager.sync().expect("sync() should succeed");
661        }
662
663        let dwb = dwb_path_for(&path);
664        fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
665
666        let err = match Pager::open_default(&path) {
667            Ok(_) => panic!("retired DWB sidecar must refuse open"),
668            Err(err) => err,
669        };
670        assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
671
672        cleanup(&path);
673    }
674
675    #[test]
676    fn interrupted_dwb_staging_write_is_discarded_on_open() {
677        let path = temp_db_path();
678        cleanup(&path);
679
680        {
681            let pager = Pager::open_default(&path).expect("open_default() should succeed");
682            pager.sync().expect("sync() should succeed");
683        }
684
685        let mut file = OpenOptions::new()
686            .read(true)
687            .write(true)
688            .open(&path)
689            .expect("open() should succeed");
690        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
691            .expect("seek() should succeed");
692        file.write_all(b"RDDW")
693            .expect("write corrupt DWB magic should succeed");
694        file.sync_all().expect("sync_all() should succeed");
695
696        {
697            let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
698            pager.sync().expect("sync() should succeed");
699        }
700
701        let mut cleared = [0u8; 4];
702        let mut file = OpenOptions::new()
703            .read(true)
704            .open(&path)
705            .expect("open() should succeed");
706        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
707            .expect("seek() should succeed");
708        file.read_exact(&mut cleared)
709            .expect("read_exact() should succeed");
710        assert_ne!(cleared, reddb_file::DWB_MAGIC);
711
712        cleanup(&path);
713    }
714
715    #[test]
716    fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
717        assert_eq!(
718            classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
719            Some(CowFilesystemKind::Zfs),
720            "ZFS is always CoW"
721        );
722        assert_eq!(
723            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
724            Some(CowFilesystemKind::BtrfsDataCow),
725            "btrfs qualifies only when datacow remains enabled"
726        );
727        assert_eq!(
728            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
729            None,
730            "btrfs nodatacow mount option must reject DWB skip"
731        );
732        assert_eq!(
733            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
734            None,
735            "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
736        );
737        assert_eq!(
738            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
739            None,
740            "missing btrfs inode flags are uncertain and must fail closed"
741        );
742        assert_eq!(
743            classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
744            None,
745            "missing btrfs mount options are uncertain and must fail closed"
746        );
747    }
748
749    #[cfg(target_os = "linux")]
750    #[test]
751    fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
752        let mountinfo = "\
75324 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
75435 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
75536 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
756";
757
758        assert_eq!(
759            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
760                .as_deref(),
761            Some("rw,relatime,rw,space_cache=v2")
762        );
763        assert_eq!(
764            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
765                .as_deref(),
766            Some("rw,relatime,rw,nodatacow")
767        );
768    }
769
770    #[test]
771    fn double_write_false_keeps_dwb_when_cow_probe_denies() {
772        let _override = cow_atomic_write_override(false);
773        let path = temp_db_path();
774        cleanup(&path);
775
776        {
777            let config = PagerConfig {
778                double_write: false,
779                ..Default::default()
780            };
781            let pager = Pager::open(&path, config).expect("open() should succeed");
782            let page = pager
783                .allocate_page(PageType::BTreeLeaf)
784                .expect("allocate_page() should succeed");
785            pager
786                .write_page(page.page_id(), page)
787                .expect("write_page() should succeed");
788            pager.flush().expect("flush() should succeed");
789        }
790
791        assert!(
792            !dwb_path_for(&path).exists(),
793            "DWB must stay enabled in-file when double_write=false is not proven safe"
794        );
795
796        cleanup(&path);
797    }
798
799    #[test]
800    fn double_write_false_skips_dwb_when_cow_probe_allows() {
801        let _override = cow_atomic_write_override(true);
802        let path = temp_db_path();
803        cleanup(&path);
804
805        {
806            let config = PagerConfig {
807                double_write: false,
808                ..Default::default()
809            };
810            let pager = Pager::open(&path, config).expect("open() should succeed");
811            let page = pager
812                .allocate_page(PageType::BTreeLeaf)
813                .expect("allocate_page() should succeed");
814            pager
815                .write_page(page.page_id(), page)
816                .expect("write_page() should succeed");
817            pager.flush().expect("flush() should succeed");
818        }
819
820        assert!(
821            !dwb_path_for(&path).exists(),
822            "DWB may be skipped only after the CoW probe allows it"
823        );
824
825        cleanup(&path);
826    }
827
828    #[test]
829    fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
830        let _override = cow_atomic_write_override(true);
831        let path = temp_db_path();
832        cleanup(&path);
833
834        let page_id;
835        {
836            let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
837            let page = pager
838                .allocate_page(PageType::BTreeLeaf)
839                .expect("allocate_page() should succeed");
840            page_id = page.page_id();
841            pager.sync().expect("sync() should succeed");
842        }
843
844        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
845        recovered_page
846            .insert_cell(b"key", b"value")
847            .expect("insert_cell() should succeed");
848        write_dwb_fixture(&path, &[(page_id, recovered_page)]);
849
850        {
851            let config = PagerConfig {
852                double_write: false,
853                ..Default::default()
854            };
855            let pager = Pager::open(&path, config).expect("open() should succeed");
856            let read_page = pager
857                .read_page(page_id)
858                .expect("read_page() should succeed");
859            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
860            assert_eq!(key, b"key");
861            assert_eq!(value, b"value");
862        }
863
864        assert!(!dwb_path_for(&path).exists());
865
866        cleanup(&path);
867    }
868
869    #[test]
870    fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
871        let _override = cow_atomic_write_override(true);
872        let path = temp_db_path();
873        cleanup(&path);
874
875        let config = PagerConfig {
876            double_write: false,
877            ..Default::default()
878        };
879
880        let page_id;
881        let before;
882        let after;
883        {
884            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
885            let mut page = pager
886                .allocate_page(PageType::BTreeLeaf)
887                .expect("allocate_page() should succeed");
888            page_id = page.page_id();
889            page.insert_cell(b"phase", b"before")
890                .expect("insert_cell() should succeed");
891            pager
892                .write_page(page_id, page)
893                .expect("write_page() should succeed");
894            pager.sync().expect("sync() should succeed");
895            before = pager
896                .read_page(page_id)
897                .expect("read_page() should succeed");
898
899            let mut page = before.clone();
900            page.insert_cell(b"phase2", b"after")
901                .expect("insert_cell() should succeed");
902            pager
903                .write_page(page_id, page)
904                .expect("write_page() should succeed");
905            pager.flush().expect("flush() should succeed");
906            after = pager
907                .read_page(page_id)
908                .expect("read_page() should succeed");
909        }
910
911        // CoW crash model: the interrupted write leaves either the old full
912        // page or the new full page, never a torn mix. Exercise both outcomes.
913        for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
914            write_page_bytes(&path, page_id, whole_page);
915
916            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
917            let recovered = pager
918                .read_page(page_id)
919                .expect("read_page() should succeed");
920            assert_eq!(recovered.cell_count(), expected_cells);
921            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
922            assert_eq!(key, b"phase");
923            assert_eq!(value, b"before");
924            if expected_cells == 2 {
925                let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
926                assert_eq!(key, b"phase2");
927                assert_eq!(value, b"after");
928            }
929            drop(pager);
930        }
931
932        cleanup(&path);
933    }
934
935    #[test]
936    fn same_mid_write_without_cow_recovers_from_dwb() {
937        let _override = cow_atomic_write_override(false);
938        let path = temp_db_path();
939        cleanup(&path);
940
941        let config = PagerConfig {
942            double_write: false,
943            ..Default::default()
944        };
945
946        let page_id;
947        let before;
948        let after;
949        {
950            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
951            let mut page = pager
952                .allocate_page(PageType::BTreeLeaf)
953                .expect("allocate_page() should succeed");
954            page_id = page.page_id();
955            page.insert_cell(b"phase", b"before")
956                .expect("insert_cell() should succeed");
957            pager
958                .write_page(page_id, page)
959                .expect("write_page() should succeed");
960            pager.sync().expect("sync() should succeed");
961            before = pager
962                .read_page(page_id)
963                .expect("read_page() should succeed");
964
965            let mut page = before.clone();
966            page.insert_cell(b"phase2", b"after")
967                .expect("insert_cell() should succeed");
968            pager
969                .write_page(page_id, page)
970                .expect("write_page() should succeed");
971            pager.flush().expect("flush() should succeed");
972            after = pager
973                .read_page(page_id)
974                .expect("read_page() should succeed");
975        }
976
977        write_dwb_fixture(&path, &[(page_id, after.clone())]);
978        write_torn_page_bytes(&path, page_id, &before, &after);
979
980        {
981            let pager = Pager::open(&path, config).expect("open() should succeed");
982            let recovered = pager
983                .read_page(page_id)
984                .expect("read_page() should succeed");
985            assert_eq!(recovered.cell_count(), 2);
986
987            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
988            assert_eq!(key, b"phase");
989            assert_eq!(value, b"before");
990
991            let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
992            assert_eq!(key, b"phase2");
993            assert_eq!(value, b"after");
994        }
995
996        assert!(!dwb_path_for(&path).exists());
997        cleanup(&path);
998    }
999
1000    // -----------------------------------------------------------------
1001    // Target 3: WAL-first flush ordering
1002    // -----------------------------------------------------------------
1003
1004    #[test]
1005    fn pager_starts_without_wal_writer() {
1006        let path = temp_db_path();
1007        let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1008        assert!(!pager.has_wal_writer());
1009        drop(pager);
1010        cleanup(&path);
1011    }
1012
1013    #[test]
1014    fn set_wal_writer_attaches_handle() {
1015        use crate::storage::wal::writer::WalWriter;
1016        use std::sync::{Arc, Mutex};
1017
1018        let db_path = temp_db_path();
1019        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1020        let _ = fs::remove_file(&wal_path);
1021
1022        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1023        let wal = Arc::new(Mutex::new(
1024            WalWriter::open(&wal_path).expect("open() should succeed"),
1025        ));
1026        pager.set_wal_writer(Arc::clone(&wal));
1027        assert!(pager.has_wal_writer());
1028
1029        pager.clear_wal_writer();
1030        assert!(!pager.has_wal_writer());
1031
1032        drop(pager);
1033        let _ = fs::remove_file(&wal_path);
1034        cleanup(&db_path);
1035    }
1036
1037    #[test]
1038    fn flush_with_lsn_zero_pages_skips_wal_call() {
1039        // When every dirty page has lsn == 0 (the legacy auto-commit
1040        // path), flush() must NOT call wal.flush_until — there is no
1041        // WAL record to wait for. We verify this by attaching a WAL
1042        // whose durable_lsn starts at 8 and confirming flush() does
1043        // not advance it (no append, no flush).
1044        use crate::storage::wal::writer::WalWriter;
1045        use std::sync::{Arc, Mutex};
1046
1047        let db_path = temp_db_path();
1048        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1049        let _ = fs::remove_file(&wal_path);
1050
1051        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1052        let wal = Arc::new(Mutex::new(
1053            WalWriter::open(&wal_path).expect("open() should succeed"),
1054        ));
1055        let initial_durable = {
1056            let g = wal.lock().expect("lock() should succeed");
1057            g.durable_lsn()
1058        };
1059        pager.set_wal_writer(Arc::clone(&wal));
1060
1061        // Allocate and write a page with lsn = 0.
1062        let mut page = pager
1063            .allocate_page(PageType::BTreeLeaf)
1064            .expect("allocate_page() should succeed");
1065        page.insert_cell(b"k", b"v")
1066            .expect("insert_cell() should succeed");
1067        // header.lsn stays at 0 — caller did not stamp.
1068        pager
1069            .write_page(page.page_id(), page)
1070            .expect("write_page() should succeed");
1071        pager.flush().expect("flush() should succeed");
1072
1073        // WAL durable_lsn must be unchanged because flush_until was
1074        // never called (max lsn over dirty pages was 0).
1075        let after_flush = {
1076            let g = wal.lock().expect("lock() should succeed");
1077            g.durable_lsn()
1078        };
1079        assert_eq!(after_flush, initial_durable);
1080
1081        drop(pager);
1082        let _ = fs::remove_file(&wal_path);
1083        cleanup(&db_path);
1084    }
1085
1086    #[test]
1087    fn flush_advances_wal_durable_when_pages_carry_lsn() {
1088        // The full WAL-first dance: append a record, capture the
1089        // returned LSN, stamp it on a page, flush — afterwards the
1090        // WAL must be durable up to at least that LSN.
1091        use crate::storage::wal::record::WalRecord;
1092        use crate::storage::wal::writer::WalWriter;
1093        use std::sync::{Arc, Mutex};
1094
1095        let db_path = temp_db_path();
1096        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1097        let _ = fs::remove_file(&wal_path);
1098
1099        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1100        let wal = Arc::new(Mutex::new(
1101            WalWriter::open(&wal_path).expect("open() should succeed"),
1102        ));
1103        pager.set_wal_writer(Arc::clone(&wal));
1104
1105        // Stamp two dirty pages with a real WAL LSN.
1106        let stamped_lsn = {
1107            let mut wal_guard = wal.lock().expect("lock() should succeed");
1108            wal_guard
1109                .append(&WalRecord::Begin { tx_id: 1 })
1110                .expect("append() should succeed");
1111            wal_guard
1112                .append(&WalRecord::Commit { tx_id: 1 })
1113                .expect("append() should succeed");
1114            wal_guard.current_lsn()
1115        };
1116        let mut page = pager
1117            .allocate_page(PageType::BTreeLeaf)
1118            .expect("allocate_page() should succeed");
1119        page.insert_cell(b"k", b"v")
1120            .expect("insert_cell() should succeed");
1121        // Use the public Page API to set the LSN.
1122        page.set_lsn(stamped_lsn);
1123        pager
1124            .write_page(page.page_id(), page)
1125            .expect("write_page() should succeed");
1126        pager.flush().expect("flush() should succeed");
1127
1128        // After flush, the WAL is durable at least up to our stamp.
1129        let after_flush = {
1130            let g = wal.lock().expect("lock() should succeed");
1131            g.durable_lsn()
1132        };
1133        assert!(
1134            after_flush >= stamped_lsn,
1135            "after flush durable_lsn {} must be >= stamped {}",
1136            after_flush,
1137            stamped_lsn
1138        );
1139
1140        drop(pager);
1141        let _ = fs::remove_file(&wal_path);
1142        cleanup(&db_path);
1143    }
1144
1145    // -----------------------------------------------------------------
1146    // gh-892: filesystem block-size alignment diagnostic
1147    // -----------------------------------------------------------------
1148
1149    #[test]
1150    fn block_size_warn_fires_for_mismatched_block_size() {
1151        // A block size that does not divide the 16 KiB page size means a
1152        // page write straddles FS blocks — the predicate must report a
1153        // misalignment so `open()` emits the warning.
1154        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1155        // Block larger than the page (e.g. 1 MiB): 16384 % 1048576 != 0.
1156        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1157        // 6 KiB also fails to divide 16 KiB.
1158        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1159    }
1160
1161    #[test]
1162    fn block_size_silent_for_divisor() {
1163        // Block sizes that evenly divide the page size: no straddle, no warn.
1164        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1165        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1166        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1167        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1168    }
1169
1170    #[test]
1171    fn block_size_unavailable_is_silent() {
1172        // st_blksize == 0 means the probe is unavailable; never warn on it.
1173        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1174    }
1175
1176    #[test]
1177    fn page_size_is_unchanged_16kib() {
1178        // The diagnostic must never alter the compile-time page size.
1179        assert_eq!(PAGE_SIZE, 16 * 1024);
1180    }
1181}