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