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