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