Skip to main content

reddb_server/storage/engine/
pager.rs

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