Skip to main content

clt_database/storage/
pager.rs

1use crate::assert::assert_send_sync;
2#[cfg(target_vendor = "apple")]
3use crate::io::AtomicFileSyncType;
4use crate::io::FileSyncType;
5use crate::io::WriteBatch;
6use crate::storage::btree::PinGuard;
7use crate::storage::subjournal::Subjournal;
8use crate::storage::wal::{CheckpointLockSource, PreparedFrames};
9use crate::storage::{
10    buffer_pool::BufferPool,
11    database::DatabaseStorage,
12    sqlite3_ondisk::{
13        self, parse_wal_frame_header, DatabaseHeader, OverflowCell, PageSize, PageType,
14        CELL_PTR_SIZE_BYTES, INTERIOR_PAGE_HEADER_SIZE_BYTES, LEAF_PAGE_HEADER_SIZE_BYTES,
15        MINIMUM_CELL_SIZE,
16    },
17    wal::{CheckpointResult, RollbackTo, Wal, IOV_MAX},
18};
19use crate::sync::atomic::{
20    AtomicBool, AtomicIsize, AtomicU16, AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering,
21};
22use crate::sync::Arc;
23use crate::sync::{Mutex, RwLock};
24use crate::types::{IOCompletions, WalState};
25use crate::util::IOExt as _;
26use crate::{
27    io::CompletionGroup, return_if_io, types::WalFrameInfo, Completion, Connection, IOResult,
28    LimboError, Result, TransactionState,
29};
30use crate::{io_yield_one, Buffer, CompletionError, IOContext, OpenFlags, SyncMode, IO};
31#[allow(unused_imports)]
32use crate::{
33    turso_assert, turso_assert_eq, turso_assert_greater_than, turso_assert_greater_than_or_equal,
34    turso_assert_less_than, turso_assert_ne, turso_debug_assert, turso_soft_unreachable,
35};
36use arc_swap::ArcSwapOption;
37use roaring::RoaringBitmap;
38use std::cell::UnsafeCell;
39use std::collections::HashMap;
40use tracing::{instrument, trace, Level};
41
42use super::btree::offset::{
43    BTREE_CELL_CONTENT_AREA, BTREE_CELL_COUNT, BTREE_FIRST_FREEBLOCK, BTREE_FRAGMENTED_BYTES_COUNT,
44    BTREE_PAGE_TYPE, BTREE_RIGHTMOST_PTR,
45};
46use super::btree::{
47    btree_init_page, payload_overflow_threshold_max, payload_overflow_threshold_min,
48};
49use super::page_cache::{CacheError, CacheResizeResult, PageCache, PageCacheKey, SpillResult};
50use super::sqlite3_ondisk::read_varint;
51use super::sqlite3_ondisk::{
52    begin_write_btree_page, read_btree_cell, read_u32, BTreeCell, FREELIST_LEAF_PTR_SIZE,
53    FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR, FREELIST_TRUNK_OFFSET_LEAF_COUNT,
54    FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR,
55};
56use super::wal::{CheckpointMode, WalAutoActions};
57use crate::storage::encryption::{CipherMode, EncryptionContext, EncryptionKey};
58
59/// SQLite's default maximum page count
60const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe;
61const RESERVED_SPACE_NOT_SET: u16 = u16::MAX;
62
63#[cfg(clt_turso_feature = "test_helper")]
64/// Used for testing purposes to change the position of the PENDING BYTE
65static PENDING_BYTE: AtomicU32 = AtomicU32::new(0x40000000);
66
67#[cfg(not(clt_turso_feature = "test_helper"))]
68/// Byte offset that signifies the start of the ignored page - 1 GB mark
69const PENDING_BYTE: u32 = 0x40000000;
70
71#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
72use ptrmap::*;
73
74#[derive(Debug, Clone)]
75pub struct HeaderRef(PageRef);
76
77impl HeaderRef {
78    pub fn from_pager(pager: &Pager) -> Result<IOResult<Self>> {
79        let page = return_if_io!(pager.read_header_page());
80        Ok(IOResult::Done(Self(page)))
81    }
82
83    pub fn borrow(&self) -> &DatabaseHeader {
84        // TODO: Instead of erasing mutability, implement `get_mut_contents` and return a shared reference.
85        let content = self.0.get_contents();
86        bytemuck::from_bytes::<DatabaseHeader>(&content.as_ptr()[0..DatabaseHeader::SIZE])
87    }
88}
89
90#[derive(Debug, Clone)]
91pub struct HeaderRefMut(PageRef);
92
93impl HeaderRefMut {
94    pub fn from_pager(pager: &Pager) -> Result<IOResult<Self>> {
95        let page = return_if_io!(pager.read_header_page());
96        pager.add_dirty(&page)?;
97        Ok(IOResult::Done(Self(page)))
98    }
99
100    pub fn borrow_mut(&self) -> &mut DatabaseHeader {
101        let content = self.0.get_contents();
102        bytemuck::from_bytes_mut::<DatabaseHeader>(&mut content.as_ptr()[0..DatabaseHeader::SIZE])
103    }
104
105    /// Get a reference to the underlying page
106    pub fn page(&self) -> &PageRef {
107        &self.0
108    }
109}
110
111pub struct PageInner {
112    pub flags: AtomicUsize,
113    pub id: usize,
114    /// If >0, the page is pinned and not eligible for eviction from the page cache.
115    /// The reason this is a counter is that multiple nested code paths may signal that
116    /// a page must not be evicted from the page cache, so even if an inner code path
117    /// requests unpinning via [Page::unpin], the pin count will still be >0 if the outer
118    /// code path has not yet requested to unpin the page as well.
119    ///
120    /// Note that [PageCache::clear] evicts the pages even if pinned, so as long as
121    /// we clear the page cache on errors, pins will not 'leak'.
122    pub pin_count: AtomicUsize,
123    /// The WAL frame number this page was loaded from (0 if loaded from main DB file)
124    /// This tracks which version of the page we have in memory
125    pub wal_tag: AtomicU64,
126    /// The actual page data buffer. None if not loaded.
127    pub buffer: Option<Arc<Buffer>>,
128    /// Overflow cells during btree operations
129    pub overflow_cells: Vec<OverflowCell>,
130}
131
132// Methods moved from PageContent - these provide btree page access
133impl PageInner {
134    /// Creates a new PageInner from an Arc<Buffer>.
135    pub fn new(buffer: Arc<Buffer>) -> Self {
136        Self {
137            flags: AtomicUsize::new(0),
138            id: 0,
139            pin_count: AtomicUsize::new(0),
140            wal_tag: AtomicU64::new(TAG_UNSET),
141            buffer: Some(buffer),
142            overflow_cells: Vec::new(),
143        }
144    }
145
146    /// Creates a new PageInner with an owned buffer.
147    pub fn from_buffer(buffer: Buffer) -> Self {
148        Self {
149            flags: AtomicUsize::new(0),
150            id: 0,
151            pin_count: AtomicUsize::new(0),
152            wal_tag: AtomicU64::new(TAG_UNSET),
153            buffer: Some(Arc::new(buffer)),
154            overflow_cells: Vec::new(),
155        }
156    }
157    /// Get the page buffer as a mutable slice. Panics if buffer not loaded.
158    #[inline]
159    #[allow(clippy::mut_from_ref)]
160    pub fn as_ptr(&self) -> &mut [u8] {
161        self.buffer
162            .as_ref()
163            .expect("buffer not loaded")
164            .as_mut_slice()
165    }
166
167    /// The position where page content starts. It's 100 for page 1 (database file header is 100 bytes),
168    /// 0 for all other pages.
169    #[inline]
170    pub fn offset(&self) -> usize {
171        if self.id == 1 {
172            DatabaseHeader::SIZE
173        } else {
174            0
175        }
176    }
177
178    /// Read a u8 from the page content at the given offset, taking account the possible db header on page 1.
179    #[inline]
180    fn read_u8(&self, pos: usize) -> u8 {
181        let buf = self.as_ptr();
182        buf[self.offset() + pos]
183    }
184
185    /// Read a u16 from the page content at the given offset, taking account the possible db header on page 1.
186    #[inline]
187    fn read_u16(&self, pos: usize) -> u16 {
188        let buf = self.as_ptr();
189        let offset = self.offset();
190        u16::from_be_bytes([buf[offset + pos], buf[offset + pos + 1]])
191    }
192
193    /// Read a u32 from the page content at the given offset, taking account the possible db header on page 1.
194    #[inline]
195    fn read_u32(&self, pos: usize) -> u32 {
196        let buf = self.as_ptr();
197        read_u32(buf, self.offset() + pos)
198    }
199
200    /// Write a u8 to the page content at the given offset, taking account the possible db header on page 1.
201    #[inline]
202    fn write_u8(&self, pos: usize, value: u8) {
203        tracing::trace!("write_u8(pos={}, value={})", pos, value);
204        let buf = self.as_ptr();
205        buf[self.offset() + pos] = value;
206    }
207
208    /// Write a u16 to the page content at the given offset, taking account the possible db header on page 1.
209    #[inline]
210    fn write_u16(&self, pos: usize, value: u16) {
211        tracing::trace!("write_u16(pos={}, value={})", pos, value);
212        let buf = self.as_ptr();
213        let offset = self.offset();
214        buf[offset + pos..offset + pos + 2].copy_from_slice(&value.to_be_bytes());
215    }
216
217    /// Write a u32 to the page content at the given offset, taking account the possible db header on page 1.
218    #[inline]
219    fn write_u32(&self, pos: usize, value: u32) {
220        tracing::trace!("write_u32(pos={}, value={})", pos, value);
221        let buf = self.as_ptr();
222        let offset = self.offset();
223        buf[offset + pos..offset + pos + 4].copy_from_slice(&value.to_be_bytes());
224    }
225
226    #[inline]
227    pub fn page_type(&self) -> crate::Result<PageType> {
228        self.read_u8(BTREE_PAGE_TYPE).try_into()
229    }
230
231    /// Read a u16 from the page content at the given absolute offset (no db header offset).
232    #[inline]
233    pub fn read_u16_no_offset(&self, pos: usize) -> u16 {
234        let buf = self.as_ptr();
235        u16::from_be_bytes([buf[pos], buf[pos + 1]])
236    }
237
238    /// Read a u32 from the page content at the given absolute offset (no db header offset).
239    #[inline]
240    pub fn read_u32_no_offset(&self, pos: usize) -> u32 {
241        let buf = self.as_ptr();
242        u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
243    }
244
245    /// Write a u16 at the given absolute offset (no db header offset).
246    pub fn write_u16_no_offset(&self, pos: usize, value: u16) {
247        tracing::trace!("write_u16_no_offset(pos={}, value={})", pos, value);
248        let buf = self.as_ptr();
249        buf[pos..pos + 2].copy_from_slice(&value.to_be_bytes());
250    }
251
252    /// Write a u32 at the given absolute offset (no db header offset).
253    pub fn write_u32_no_offset(&self, pos: usize, value: u32) {
254        tracing::trace!("write_u32_no_offset(pos={}, value={})", pos, value);
255        let buf = self.as_ptr();
256        buf[pos..pos + 4].copy_from_slice(&value.to_be_bytes());
257    }
258
259    pub fn write_page_type(&self, value: u8) {
260        self.write_u8(BTREE_PAGE_TYPE, value);
261    }
262
263    pub fn write_rightmost_ptr(&self, value: u32) {
264        self.write_u32(BTREE_RIGHTMOST_PTR, value);
265    }
266
267    pub fn write_first_freeblock(&self, value: u16) {
268        self.write_u16(BTREE_FIRST_FREEBLOCK, value);
269    }
270
271    pub fn write_freeblock(&self, offset: u16, size: u16, next_block: Option<u16>) {
272        self.write_freeblock_next_ptr(offset, next_block.unwrap_or(0));
273        self.write_freeblock_size(offset, size);
274    }
275
276    pub fn write_freeblock_size(&self, offset: u16, size: u16) {
277        self.write_u16_no_offset(offset as usize + 2, size);
278    }
279
280    pub fn write_freeblock_next_ptr(&self, offset: u16, next_block: u16) {
281        self.write_u16_no_offset(offset as usize, next_block);
282    }
283
284    pub fn read_freeblock(&self, offset: u16) -> (u16, u16) {
285        (
286            self.read_u16_no_offset(offset as usize),
287            self.read_u16_no_offset(offset as usize + 2),
288        )
289    }
290
291    pub fn write_cell_count(&self, value: u16) {
292        self.write_u16(BTREE_CELL_COUNT, value);
293    }
294
295    pub fn write_cell_content_area(&self, value: usize) {
296        turso_debug_assert!(value <= PageSize::MAX as usize);
297        let value = value as u16;
298        self.write_u16(BTREE_CELL_CONTENT_AREA, value);
299    }
300
301    pub fn write_fragmented_bytes_count(&self, value: u8) {
302        self.write_u8(BTREE_FRAGMENTED_BYTES_COUNT, value);
303    }
304
305    #[inline]
306    pub fn first_freeblock(&self) -> u16 {
307        self.read_u16(BTREE_FIRST_FREEBLOCK)
308    }
309
310    #[inline]
311    pub fn cell_count(&self) -> usize {
312        self.read_u16(BTREE_CELL_COUNT) as usize
313    }
314
315    #[inline]
316    pub fn cell_pointer_array_size(&self) -> usize {
317        self.cell_count() * CELL_PTR_SIZE_BYTES
318    }
319
320    #[inline]
321    pub fn unallocated_region_start(&self) -> usize {
322        let (cell_ptr_array_start, cell_ptr_array_size) = self.cell_pointer_array_offset_and_size();
323        cell_ptr_array_start + cell_ptr_array_size
324    }
325
326    #[inline]
327    pub fn unallocated_region_size(&self) -> usize {
328        self.cell_content_area() as usize - self.unallocated_region_start()
329    }
330
331    #[inline]
332    pub fn cell_content_area(&self) -> u32 {
333        let offset = self.read_u16(BTREE_CELL_CONTENT_AREA);
334        if offset == 0 {
335            PageSize::MAX
336        } else {
337            offset as u32
338        }
339    }
340
341    #[inline]
342    pub fn header_size(&self) -> usize {
343        let is_interior = self.read_u8(BTREE_PAGE_TYPE) <= PageType::TableInterior as u8;
344        (!is_interior as usize) * LEAF_PAGE_HEADER_SIZE_BYTES
345            + (is_interior as usize) * INTERIOR_PAGE_HEADER_SIZE_BYTES
346    }
347
348    #[inline]
349    pub fn num_frag_free_bytes(&self) -> u8 {
350        self.read_u8(BTREE_FRAGMENTED_BYTES_COUNT)
351    }
352
353    #[inline]
354    pub fn rightmost_pointer(&self) -> crate::Result<Option<u32>> {
355        match self.page_type()? {
356            PageType::IndexInterior | PageType::TableInterior => {
357                Ok(Some(self.read_u32(BTREE_RIGHTMOST_PTR)))
358            }
359            PageType::IndexLeaf | PageType::TableLeaf => Ok(None),
360        }
361    }
362
363    #[inline]
364    pub fn rightmost_pointer_raw(&self) -> crate::Result<Option<*mut u8>> {
365        match self.page_type()? {
366            PageType::IndexInterior | PageType::TableInterior => Ok(Some(unsafe {
367                self.as_ptr()
368                    .as_mut_ptr()
369                    .add(self.offset() + BTREE_RIGHTMOST_PTR)
370            })),
371            PageType::IndexLeaf | PageType::TableLeaf => Ok(None),
372        }
373    }
374
375    #[inline]
376    pub fn cell_get(&self, idx: usize, usable_size: usize) -> crate::Result<BTreeCell> {
377        tracing::trace!("cell_get(idx={})", idx);
378        let buf = self.as_ptr();
379
380        let ncells = self.cell_count();
381        turso_assert_less_than!(idx, ncells,
382            "cell_get: idx out of bounds",
383            {"idx": idx, "ncells": ncells}
384        );
385        let cell_pointer_array_start = self.header_size();
386        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
387        let cell_pointer = self.read_u16(cell_pointer) as usize;
388
389        let static_buf: &'static [u8] = unsafe { std::mem::transmute::<&[u8], &'static [u8]>(buf) };
390        read_btree_cell(static_buf, self, cell_pointer, usable_size)
391    }
392
393    #[inline(always)]
394    pub fn cell_table_interior_read_rowid(&self, idx: usize) -> crate::Result<i64> {
395        turso_debug_assert!(matches!(self.page_type(), Ok(PageType::TableInterior)));
396        let buf = self.as_ptr();
397        let cell_pointer_array_start = self.header_size();
398        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
399        let cell_pointer = self.read_u16(cell_pointer) as usize;
400        const LEFT_CHILD_PAGE_SIZE_BYTES: usize = 4;
401        let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(
402            buf,
403            cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES..
404        ))?;
405        Ok(rowid as i64)
406    }
407
408    #[inline(always)]
409    pub fn cell_interior_read_left_child_page(&self, idx: usize) -> crate::Result<u32> {
410        turso_debug_assert!(matches!(
411            self.page_type(),
412            Ok(PageType::TableInterior) | Ok(PageType::IndexInterior)
413        ));
414        let buf = self.as_ptr();
415        let cell_pointer_array_start = self.header_size();
416        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
417        let cell_pointer = self.read_u16(cell_pointer) as usize;
418        crate::assert_or_bail_corrupt!(
419            cell_pointer + 4 <= buf.len(),
420            "cell pointer {} out of bounds for page size {}",
421            cell_pointer,
422            buf.len()
423        );
424        Ok(u32::from_be_bytes([
425            buf[cell_pointer],
426            buf[cell_pointer + 1],
427            buf[cell_pointer + 2],
428            buf[cell_pointer + 3],
429        ]))
430    }
431
432    #[inline(always)]
433    pub fn cell_table_leaf_read_rowid(&self, idx: usize) -> crate::Result<i64> {
434        turso_debug_assert!(matches!(self.page_type(), Ok(PageType::TableLeaf)));
435        let buf = self.as_ptr();
436        let cell_pointer_array_start = self.header_size();
437        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
438        let cell_pointer = self.read_u16(cell_pointer) as usize;
439        let mut pos = cell_pointer;
440        let (_, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?;
441        pos += nr;
442        let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?;
443        Ok(rowid as i64)
444    }
445
446    /// Fast path for index cells: returns payload slice and overflow info without constructing BTreeCell.
447    ///
448    /// This bypasses the full `cell_get()` to `read_btree_cell()` path for binary search hot loops.
449    /// The returned slice is valid as long as the page is alive.
450    ///
451    /// Returns: (payload_slice, payload_size, first_overflow_page)
452    #[inline(always)]
453    pub fn cell_index_read_payload_ptr(
454        &self,
455        idx: usize,
456        usable_size: usize,
457    ) -> crate::Result<(&'static [u8], u64, Option<u32>)> {
458        let buf = self.as_ptr();
459        let cell_pointer_array_start = self.header_size();
460        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
461        let cell_offset = self.read_u16(cell_pointer) as usize;
462
463        let page_type = self.page_type()?;
464        let (payload_size, varint_len, header_skip) = match page_type {
465            PageType::IndexInterior => {
466                let (size, len) =
467                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset + 4..))?;
468                (size, len, 4usize)
469            }
470            PageType::IndexLeaf => {
471                let (size, len) =
472                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset..))?;
473                (size, len, 0usize)
474            }
475            _ => unreachable!("cell_index_read_payload_ptr called on non-index page"),
476        };
477
478        let payload_start = cell_offset + header_skip + varint_len;
479
480        let max_local = payload_overflow_threshold_max(page_type, usable_size);
481        let min_local = payload_overflow_threshold_min(page_type, usable_size);
482        let (overflows, local_size) = sqlite3_ondisk::payload_overflows(
483            payload_size as usize,
484            max_local,
485            min_local,
486            usable_size,
487        );
488
489        let (payload_slice, first_overflow) = if overflows {
490            let overflow_ptr_offset = payload_start + local_size - 4;
491            crate::assert_or_bail_corrupt!(
492                overflow_ptr_offset + 4 <= buf.len(),
493                "overflow pointer offset {} out of bounds for page size {}",
494                overflow_ptr_offset,
495                buf.len()
496            );
497            let first_overflow_page = u32::from_be_bytes([
498                buf[overflow_ptr_offset],
499                buf[overflow_ptr_offset + 1],
500                buf[overflow_ptr_offset + 2],
501                buf[overflow_ptr_offset + 3],
502            ]);
503            let payload_end = payload_start + local_size - 4;
504            crate::assert_or_bail_corrupt!(
505                payload_start < payload_end && payload_end <= buf.len(),
506                "payload range {}..{} out of bounds for page size {}",
507                payload_start,
508                payload_end,
509                buf.len()
510            );
511            // SAFETY: valid as long as page is alive
512            let slice = unsafe {
513                std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end])
514            };
515            (slice, Some(first_overflow_page))
516        } else {
517            let payload_end = payload_start + payload_size as usize;
518            crate::assert_or_bail_corrupt!(
519                payload_end <= buf.len(),
520                "payload range {}..{} out of bounds for page size {}",
521                payload_start,
522                payload_end,
523                buf.len()
524            );
525            // SAFETY: valid as long as page is alive
526            let slice = unsafe {
527                std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end])
528            };
529            (slice, None)
530        };
531
532        Ok((payload_slice, payload_size, first_overflow))
533    }
534
535    #[inline]
536    pub fn cell_pointer_array_offset_and_size(&self) -> (usize, usize) {
537        (
538            self.cell_pointer_array_offset(),
539            self.cell_pointer_array_size(),
540        )
541    }
542
543    #[inline]
544    pub fn cell_pointer_array_offset(&self) -> usize {
545        self.offset() + self.header_size()
546    }
547
548    #[inline]
549    pub fn cell_get_raw_start_offset(&self, idx: usize) -> usize {
550        let cell_pointer_array_start = self.cell_pointer_array_offset();
551        let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
552        self.read_u16_no_offset(cell_pointer) as usize
553    }
554
555    #[inline]
556    pub fn cell_get_raw_region(
557        &self,
558        idx: usize,
559        usable_size: usize,
560    ) -> crate::Result<(usize, usize)> {
561        let page_type = self.page_type()?;
562        let max_local = payload_overflow_threshold_max(page_type, usable_size);
563        let min_local = payload_overflow_threshold_min(page_type, usable_size);
564        let cell_count = self.cell_count();
565        self._cell_get_raw_region_faster(
566            idx,
567            usable_size,
568            cell_count,
569            max_local,
570            min_local,
571            page_type,
572        )
573    }
574
575    #[inline]
576    pub fn _cell_get_raw_region_faster(
577        &self,
578        idx: usize,
579        usable_size: usize,
580        cell_count: usize,
581        max_local: usize,
582        min_local: usize,
583        page_type: PageType,
584    ) -> crate::Result<(usize, usize)> {
585        let buf = self.as_ptr();
586        turso_assert_less_than!(idx, cell_count);
587        let start = self.cell_get_raw_start_offset(idx);
588        let len = match page_type {
589            PageType::IndexInterior => {
590                let (len_payload, n_payload) =
591                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?;
592                let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
593                    len_payload as usize,
594                    max_local,
595                    min_local,
596                    usable_size,
597                );
598                if overflows {
599                    4 + to_read + n_payload
600                } else {
601                    4 + len_payload as usize + n_payload
602                }
603            }
604            PageType::TableInterior => {
605                let (_, n_rowid) =
606                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?;
607                4 + n_rowid
608            }
609            PageType::IndexLeaf => {
610                let (len_payload, n_payload) =
611                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?;
612                let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
613                    len_payload as usize,
614                    max_local,
615                    min_local,
616                    usable_size,
617                );
618                if overflows {
619                    to_read + n_payload
620                } else {
621                    let mut size = len_payload as usize + n_payload;
622                    if size < MINIMUM_CELL_SIZE {
623                        size = MINIMUM_CELL_SIZE;
624                    }
625                    size
626                }
627            }
628            PageType::TableLeaf => {
629                let (len_payload, n_payload) =
630                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?;
631                let (_, n_rowid) =
632                    read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + n_payload..))?;
633                let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
634                    len_payload as usize,
635                    max_local,
636                    min_local,
637                    usable_size,
638                );
639                if overflows {
640                    to_read + n_payload + n_rowid
641                } else {
642                    let mut size = len_payload as usize + n_payload + n_rowid;
643                    if size < MINIMUM_CELL_SIZE {
644                        size = MINIMUM_CELL_SIZE;
645                    }
646                    size
647                }
648            }
649        };
650        crate::assert_or_bail_corrupt!(
651            start + len <= buf.len(),
652            "cell region {}..{} out of bounds for page size {}",
653            start,
654            start + len,
655            buf.len()
656        );
657        Ok((start, len))
658    }
659
660    pub fn is_leaf(&self) -> bool {
661        self.read_u8(BTREE_PAGE_TYPE) > PageType::TableInterior as u8
662    }
663
664    pub fn write_database_header(&self, header: &DatabaseHeader) {
665        let buf = self.as_ptr();
666        buf[0..DatabaseHeader::SIZE].copy_from_slice(bytemuck::bytes_of(header));
667    }
668
669    pub fn debug_print_freelist(&self, usable_space: usize) {
670        let mut pc = self.first_freeblock() as usize;
671        let mut block_num = 0;
672        println!("---- Free List Blocks ----");
673        println!("first freeblock pointer: {pc}");
674        println!("cell content area: {}", self.cell_content_area());
675        println!("fragmented bytes: {}", self.num_frag_free_bytes());
676
677        while pc != 0 && pc <= usable_space {
678            let next = self.read_u16_no_offset(pc);
679            let size = self.read_u16_no_offset(pc + 2);
680
681            println!("block {block_num}: position={pc}, size={size}, next={next}");
682            pc = next as usize;
683            block_num += 1;
684        }
685        println!("--------------");
686    }
687}
688
689/// Type alias for backward compatibility - PageContent is now PageInner
690pub type PageContent = PageInner;
691
692/// WAL tag not set
693pub const TAG_UNSET: u64 = u64::MAX;
694/// WAL write in progress, sentinel value set before starting a WAL write
695/// so we can detect if page was modified during the write
696pub const TAG_WRITE_PENDING: u64 = u64::MAX - 1;
697
698/// Bit layout:
699/// epoch: 20
700/// frame: 44
701const EPOCH_BITS: u32 = 20;
702const FRAME_BITS: u32 = 64 - EPOCH_BITS;
703const EPOCH_SHIFT: u32 = FRAME_BITS;
704const EPOCH_MAX: u32 = (1u32 << EPOCH_BITS) - 1;
705const FRAME_MAX: u64 = (1u64 << FRAME_BITS) - 1;
706
707#[inline]
708pub fn pack_tag_pair(frame: u64, seq: u32) -> u64 {
709    ((seq as u64) << EPOCH_SHIFT) | (frame & FRAME_MAX)
710}
711
712#[inline]
713pub fn unpack_tag_pair(tag: u64) -> (u64, u32) {
714    let epoch = ((tag >> EPOCH_SHIFT) & (EPOCH_MAX as u64)) as u32;
715    let frame = tag & FRAME_MAX;
716    (frame, epoch)
717}
718
719#[derive(Debug)]
720pub struct Page {
721    pub inner: UnsafeCell<PageInner>,
722}
723
724// SAFETY: Page is thread-safe because we use atomic page flags to serialize
725// concurrent modifications.
726unsafe impl Send for Page {}
727unsafe impl Sync for Page {}
728crate::assert::assert_send_sync!(Page);
729
730// Concurrency control of pages will be handled by the pager, we won't wrap Page with RwLock
731// because that is bad bad.
732pub type PageRef = Arc<Page>;
733
734/// Page is locked for I/O to prevent concurrent access.
735const PAGE_LOCKED: usize = 0b010;
736/// Page is dirty. Flush needed.
737const PAGE_DIRTY: usize = 0b1000;
738/// Page's contents are loaded in memory.
739const PAGE_LOADED: usize = 0b10000;
740/// Page has been spilled to WAL (can be evicted even though dirty).
741const PAGE_SPILLED: usize = 0b100000;
742
743impl Page {
744    pub fn new(id: i64) -> Self {
745        turso_assert_greater_than_or_equal!(id, 0);
746        Self {
747            inner: UnsafeCell::new(PageInner {
748                flags: AtomicUsize::new(0),
749                id: id as usize,
750                pin_count: AtomicUsize::new(0),
751                wal_tag: AtomicU64::new(TAG_UNSET),
752                buffer: None,
753                overflow_cells: Vec::new(),
754            }),
755        }
756    }
757
758    #[allow(clippy::mut_from_ref)]
759    pub fn get(&self) -> &mut PageInner {
760        unsafe { &mut *self.inner.get() }
761    }
762
763    /// Returns a mutable reference to PageInner for accessing page contents.
764    /// Panics if the page buffer is not loaded.
765    pub fn get_contents(&self) -> &mut PageInner {
766        let inner = self.get();
767        turso_debug_assert!(
768            inner.buffer.is_some(),
769            "page buffer not loaded",
770            { "page_id": inner.id }
771        );
772        inner
773    }
774
775    #[inline]
776    pub fn is_locked(&self) -> bool {
777        self.get().flags.load(Ordering::Acquire) & PAGE_LOCKED != 0
778    }
779
780    #[inline]
781    pub fn set_locked(&self) {
782        self.get().flags.fetch_or(PAGE_LOCKED, Ordering::Acquire);
783    }
784
785    #[inline]
786    pub fn clear_locked(&self) {
787        self.get().flags.fetch_and(!PAGE_LOCKED, Ordering::Release);
788    }
789
790    #[inline]
791    pub fn is_dirty(&self) -> bool {
792        self.get().flags.load(Ordering::Acquire) & PAGE_DIRTY != 0
793    }
794
795    #[inline]
796    /// almost never should be called explicitly - instead [Pager::add_dirty] method must be used
797    pub fn set_dirty(&self) {
798        tracing::debug!("set_dirty(page={})", self.get().id);
799        self.clear_wal_tag();
800        // Clear spilled flag since page is being modified again
801        self.get().flags.fetch_and(!PAGE_SPILLED, Ordering::Release);
802        self.get().flags.fetch_or(PAGE_DIRTY, Ordering::Release);
803    }
804
805    #[inline]
806    /// caller must ensure that [Pager::dirty_pages] will be updated accordingly
807    pub fn clear_dirty(&self) {
808        tracing::debug!("clear_dirty(page={})", self.get().id);
809        self.get().flags.fetch_and(!PAGE_DIRTY, Ordering::Release);
810        self.clear_wal_tag();
811    }
812
813    /// Clear the dirty flag without touching wal_tag.
814    /// Used when a WAL frame has been durably written and the tag already encodes it.
815    #[inline]
816    pub fn clear_dirty_keep_wal_tag(&self) {
817        tracing::debug!("clear_dirty_keep_wal_tag(page={})", self.get().id);
818        self.get().flags.fetch_and(!PAGE_DIRTY, Ordering::Release);
819    }
820
821    /// Returns true if the page has been spilled to WAL and is safe to evict even while dirty.
822    #[inline]
823    pub fn is_spilled(&self) -> bool {
824        self.get().flags.load(Ordering::Acquire) & PAGE_SPILLED != 0
825    }
826
827    /// Mark the page as spilled to WAL. Spilled pages remain dirty but may be evicted from cache.
828    #[inline]
829    pub fn set_spilled(&self) {
830        tracing::debug!("set_spilled(page={})", self.get().id);
831        self.get().flags.fetch_or(PAGE_SPILLED, Ordering::Release);
832    }
833
834    /// Clear the spilled flag. This is also done implicitly on set_dirty().
835    #[inline]
836    pub fn clear_spilled(&self) {
837        self.get().flags.fetch_and(!PAGE_SPILLED, Ordering::Release);
838    }
839
840    #[inline]
841    pub fn is_loaded(&self) -> bool {
842        self.get().flags.load(Ordering::Acquire) & PAGE_LOADED != 0
843    }
844
845    #[inline]
846    pub fn set_loaded(&self) {
847        self.get().flags.fetch_or(PAGE_LOADED, Ordering::Release);
848    }
849
850    #[inline]
851    pub fn clear_loaded(&self) {
852        tracing::debug!("clear loaded {}", self.get().id);
853        self.get().flags.fetch_and(!PAGE_LOADED, Ordering::Release);
854    }
855
856    #[inline]
857    pub fn is_index(&self) -> crate::Result<bool> {
858        Ok(match self.get_contents().page_type()? {
859            PageType::IndexLeaf | PageType::IndexInterior => true,
860            PageType::TableLeaf | PageType::TableInterior => false,
861        })
862    }
863
864    /// Increment the pin count by 1. A pin count >0 means the page is pinned and not eligible for eviction from the page cache.
865    #[inline]
866    pub fn pin(&self) {
867        self.get().pin_count.fetch_add(1, Ordering::SeqCst);
868    }
869
870    /// Decrement the pin count by 1. If the count reaches 0, the page is no longer
871    /// pinned and is eligible for eviction from the page cache.
872    #[inline]
873    pub fn unpin(&self) {
874        let was_pinned = self.try_unpin();
875
876        turso_assert!(
877            was_pinned,
878            "Attempted to unpin page that was not pinned",
879            { "page_id": self.get().id }
880        );
881    }
882
883    /// Try to decrement the pin count by 1, but do nothing if it was already 0.
884    /// Returns true if the pin count was decremented.
885    #[inline]
886    pub fn try_unpin(&self) -> bool {
887        self.get()
888            .pin_count
889            .fetch_update(Ordering::Release, Ordering::SeqCst, |current| {
890                if current == 0 {
891                    None
892                } else {
893                    Some(current - 1)
894                }
895            })
896            .is_ok()
897    }
898
899    /// Returns true if the page is pinned and thus not eligible for eviction from the page cache.
900    #[inline]
901    pub fn is_pinned(&self) -> bool {
902        self.get().pin_count.load(Ordering::Acquire) > 0
903    }
904
905    #[inline]
906    /// Set the WAL tag from a (frame, epoch) pair.
907    /// If inputs are invalid, stores TAG_UNSET, which will prevent
908    /// the cached page from being used during checkpoint.
909    pub fn set_wal_tag(&self, frame: u64, epoch: u32) {
910        // use only first 20 bits for seq (max: 1048576)
911        let e = epoch & EPOCH_MAX;
912        self.get()
913            .wal_tag
914            .store(pack_tag_pair(frame, e), Ordering::Release);
915    }
916
917    #[inline]
918    /// Load the (frame, seq) pair from the packed tag.
919    pub fn wal_tag_pair(&self) -> (u64, u32) {
920        unpack_tag_pair(self.get().wal_tag.load(Ordering::Acquire))
921    }
922
923    #[inline]
924    pub fn clear_wal_tag(&self) {
925        self.get().wal_tag.store(TAG_UNSET, Ordering::Release)
926    }
927
928    #[inline]
929    /// Returns true if the page has a valid WAL tag (i.e., was written to WAL and not modified since).
930    /// Returns false if the wal_tag is TAG_UNSET (page was modified since last WAL write).
931    pub fn has_wal_tag(&self) -> bool {
932        let tag = self.get().wal_tag.load(Ordering::Acquire);
933        let result = tag != TAG_UNSET && tag != TAG_WRITE_PENDING;
934        tracing::debug!(
935            "has_wal_tag(page={}) = {} (tag={:x})",
936            self.get().id,
937            result,
938            tag
939        );
940        result
941    }
942
943    #[inline]
944    /// Mark page as having a WAL write in progress.
945    /// This is set before starting a spill/cacheflush so we can detect
946    /// if the page was modified during the write.
947    pub fn set_write_pending(&self) {
948        tracing::debug!("set_write_pending(page={})", self.get().id);
949        self.get()
950            .wal_tag
951            .store(TAG_WRITE_PENDING, Ordering::Release);
952    }
953
954    #[inline]
955    /// Try to set the WAL tag, but only if the page wasn't modified during the write.
956    /// Returns true if the tag was set, false if the page was modified (wal_tag became TAG_UNSET).
957    pub fn try_set_wal_tag(&self, frame: u64, epoch: u32) -> bool {
958        let new_tag = pack_tag_pair(frame, epoch);
959        let page_id = self.get().id;
960        let current = self.get().wal_tag.load(Ordering::Acquire);
961        // Only set if current tag is not TAG_UNSET (meaning page wasn't modified during write)
962        // TAG_WRITE_PENDING is fine, it means the write was in progress and page wasn't modified
963        if current == TAG_UNSET {
964            tracing::debug!(
965                "try_set_wal_tag(page={}, frame={}) SKIPPED: wal_tag is TAG_UNSET (page was modified)",
966                page_id, frame
967            );
968            return false;
969        }
970        tracing::debug!(
971            "try_set_wal_tag(page={}, frame={}) SUCCESS: current={:x}",
972            page_id,
973            frame,
974            current
975        );
976        self.get().wal_tag.store(new_tag, Ordering::Release);
977        true
978    }
979
980    #[inline]
981    pub fn is_valid_for_checkpoint(&self, target_frame: u64, epoch: u32) -> bool {
982        let (f, s) = self.wal_tag_pair();
983        f == target_frame && s == epoch && !self.is_dirty() && self.is_loaded() && !self.is_locked()
984    }
985}
986
987#[derive(Clone, Copy, Debug, PartialEq)]
988/// The state of the current pager cache commit.
989enum CommitState {
990    /// Prepare WAL header for commit if needed
991    PrepareWal,
992    /// Sync WAL header after prepare
993    PrepareWalSync,
994    /// Get DB size (mostly from page cache - but in rare cases we can read it from disk)
995    GetDbSize,
996    /// Scan all dirty pages and issue concurrent reads for evicted (spilled) pages.
997    ScanAndIssueReads { db_size: u32 },
998    /// Wait for all batched reads of evicted pages to complete.
999    WaitBatchedReads { db_size: u32 },
1000    /// Collect pages (now all available) and prepare WAL frames.
1001    PrepareFrames { db_size: u32 },
1002    /// All frames prepared, writes are in flight
1003    WaitWrites,
1004    /// Wait for the WAL fsync that makes the commit durable. Every commit
1005    /// converges here once its writes (if any) have completed. The fsync is
1006    /// submitted here, and skipped when the WAL is not dirty (no frames
1007    /// appended since the last successful fsync) or sync_mode is not FULL.
1008    /// Commits that prepared frames continue to WalCommitDone to publish
1009    /// them; otherwise the commit finishes here, since frames written through
1010    /// `write_frame_raw` published themselves when they were appended.
1011    WaitSync,
1012    /// Finalize the WAL commit by publishing the prepared frames.
1013    /// After this state, the write transaction is durable.
1014    /// If autocheckpoint is enabled and the autocheckpoint threshold is reached, checkpoint will be attempted.
1015    WalCommitDone,
1016    /// Checkpoint the WAL to the database file (if needed).
1017    /// This is decoupled from commit - checkpoint failure does not affect commit durability.
1018    AutoCheckpoint,
1019}
1020
1021#[derive(Debug, Default)]
1022struct CheckpointState {
1023    phase: CheckpointPhase,
1024    /// The checkpoint result, set after WAL checkpoint completes
1025    result: Option<CheckpointResult>,
1026    /// The checkpoint mode, used to determine if WAL truncation is needed
1027    mode: Option<CheckpointMode>,
1028    /// The checkpoint state machine should acquire the lock or use the one by caller
1029    lock_source: CheckpointLockSource,
1030}
1031
1032#[derive(Clone, Debug)]
1033struct PendingCheckpointDbIdentityRead {
1034    max_frame: u64,
1035    header_buf: Arc<Buffer>,
1036    bytes_read: Arc<AtomicUsize>,
1037    read_sent: bool,
1038}
1039
1040#[derive(Clone, Debug, Default)]
1041enum CheckpointPhase {
1042    #[default]
1043    NotCheckpointing,
1044    Checkpoint {
1045        mode: CheckpointMode,
1046        sync_mode: crate::SyncMode,
1047        clear_page_cache: bool,
1048    },
1049    /// Truncate the database file if everything was backfilled and file is larger than expected.
1050    TruncateDbFile {
1051        sync_mode: crate::SyncMode,
1052        clear_page_cache: bool,
1053        /// Whether we've invalidated page 1 from cache (needed because checkpoint may write
1054        /// pages directly from WALto DB file, so cached page 1 of the checkpointer connection may have stale database_size)
1055        page1_invalidated: bool,
1056    },
1057    /// Sync the database file after checkpoint (if sync_mode != Off and we backfilled any frames from the WAL).
1058    SyncDbFile { clear_page_cache: bool },
1059    /// Read the synced database header before installing the durable backfill proof.
1060    ReadDbIdentity {
1061        clear_page_cache: bool,
1062        read: PendingCheckpointDbIdentityRead,
1063    },
1064    /// Wait for backend-specific durable proof sync to finish before publishing nbackfills.
1065    SyncBackfillProof {
1066        clear_page_cache: bool,
1067        max_frame: u64,
1068    },
1069    /// Publish the durable backfill progress after the proof is installed and synced.
1070    PublishBackfill {
1071        clear_page_cache: bool,
1072        max_frame: u64,
1073    },
1074    /// Truncate the WAL file after DB file is safely synced (only for TRUNCATE checkpoint mode).
1075    /// This must happen AFTER SyncDbFile to ensure data durability.
1076    TruncateWalFile { clear_page_cache: bool },
1077    /// Finalize: release guard and optionally clear page cache.
1078    Finalize { clear_page_cache: bool },
1079}
1080
1081/// The mode of allocating a btree page.
1082/// SQLite defines the following:
1083/// #define BTALLOC_ANY   0           /* Allocate any page */
1084/// #define BTALLOC_EXACT 1           /* Allocate exact page if possible */
1085/// #define BTALLOC_LE    2           /* Allocate any page <= the parameter */
1086pub enum BtreePageAllocMode {
1087    /// Allocate any btree page
1088    Any,
1089    /// Allocate a specific page number, typically used for root page allocation
1090    Exact(u32),
1091    /// Allocate a page number less than or equal to the parameter
1092    Le(u32),
1093}
1094
1095/// This will keep track of the state of current cache commit in order to not repeat work
1096struct CommitInfo {
1097    completions: Vec<Completion>,
1098    completion_group: Option<Completion>,
1099    state: CommitState,
1100    collected_pages: Vec<PageRef>,
1101    page_sources: Vec<PageSource>,
1102    page_source_cursor: usize,
1103    prepared_frames: Vec<PreparedFrames>,
1104}
1105
1106/// Represents a dirty page that will be committed to the log.
1107enum PageSource {
1108    /// Cache resident page
1109    Cached(usize),
1110    /// A page read from disk because it was spilled/evicted from cache
1111    Evicted(PageRef),
1112}
1113
1114impl CommitInfo {
1115    fn reset(&mut self) {
1116        self.completions.clear();
1117        self.completion_group = None;
1118        self.state = CommitState::PrepareWal;
1119        self.collected_pages.clear();
1120        self.page_sources.clear();
1121        self.prepared_frames.clear();
1122        self.page_source_cursor = 0;
1123    }
1124
1125    /// Clear and reserve space for n pages in each vector.
1126    fn initialize(&mut self, n: usize) {
1127        self.page_sources.clear();
1128        self.page_sources.reserve(n.min(IOV_MAX));
1129        self.completions.clear();
1130        self.completions.reserve(n / 4);
1131        self.completion_group = None;
1132        self.collected_pages.reserve(n.min(IOV_MAX));
1133    }
1134}
1135
1136/// Track the state of the auto-vacuum mode.
1137#[derive(Clone, Copy, Debug, PartialEq)]
1138pub enum AutoVacuumMode {
1139    None,
1140    Full,
1141    Incremental,
1142}
1143
1144impl From<AutoVacuumMode> for u8 {
1145    fn from(mode: AutoVacuumMode) -> u8 {
1146        match mode {
1147            AutoVacuumMode::None => 0,
1148            AutoVacuumMode::Full => 1,
1149            AutoVacuumMode::Incremental => 2,
1150        }
1151    }
1152}
1153
1154impl From<u8> for AutoVacuumMode {
1155    fn from(value: u8) -> AutoVacuumMode {
1156        match value {
1157            0 => AutoVacuumMode::None,
1158            1 => AutoVacuumMode::Full,
1159            2 => AutoVacuumMode::Incremental,
1160            _ => unreachable!("Invalid AutoVacuumMode value: {}", value),
1161        }
1162    }
1163}
1164
1165const fn auto_vacuum_header_fields(mode: AutoVacuumMode) -> (u32, u32) {
1166    match mode {
1167        AutoVacuumMode::None => (0, 0),
1168        AutoVacuumMode::Full => (1, 0),
1169        AutoVacuumMode::Incremental => (1, 1),
1170    }
1171}
1172
1173#[derive(Debug, Clone)]
1174#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1175enum PtrMapGetState {
1176    Start,
1177    Deserialize {
1178        ptrmap_page: PageRef,
1179        offset_in_ptrmap_page: usize,
1180    },
1181}
1182
1183#[derive(Debug, Clone)]
1184#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1185enum PtrMapPutState {
1186    Start,
1187    Deserialize {
1188        ptrmap_page: PageRef,
1189        offset_in_ptrmap_page: usize,
1190    },
1191}
1192
1193#[derive(Debug, Clone)]
1194enum HeaderRefState {
1195    Start,
1196    CreateHeader {
1197        page: PageRef,
1198        completion: Option<Completion>,
1199    },
1200}
1201
1202#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1203#[derive(Debug, Clone, Copy)]
1204enum BtreeCreateVacuumFullState {
1205    Start,
1206    AllocatePage { root_page_num: u32 },
1207    PtrMapPut { allocated_page_id: u32 },
1208}
1209
1210#[derive(Debug, Clone)]
1211enum SavepointKind {
1212    Statement,
1213    Named {
1214        name: String,
1215        starts_transaction: bool,
1216    },
1217}
1218
1219#[derive(Clone, Copy, Debug)]
1220pub enum SavepointResult {
1221    /// Releasing the named savepoint should commit the surrounding transaction.
1222    Commit,
1223    /// The named savepoint was released without committing the transaction.
1224    Release,
1225    /// No matching named savepoint exists.
1226    NotFound,
1227}
1228
1229/// A connection's WAL position (max frame, running frame checksum, and the
1230/// WAL generation they belong to), captured as one unit for savepoint
1231/// rollback.
1232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1233struct SavepointWalPos {
1234    max_frame: u64,
1235    checksum: (u32, u32),
1236    checkpoint_seq: u32,
1237}
1238
1239#[derive(Debug, Clone)]
1240struct SavepointSnapshot {
1241    kind: SavepointKind,
1242    start_offset: u64,
1243    db_size: u32,
1244    wal_pos: Option<SavepointWalPos>,
1245    deferred_fk_violations: isize,
1246}
1247
1248struct Savepoint {
1249    kind: SavepointKind,
1250    /// Start offset of this savepoint in the subjournal.
1251    start_offset: AtomicU64,
1252    /// Current write offset in the subjournal.
1253    write_offset: AtomicU64,
1254    /// Bitmap of page numbers that are dirty in the savepoint.
1255    page_bitmap: RwLock<RoaringBitmap>,
1256    /// Database size at the start of the savepoint.
1257    /// If the database grows during the savepoint and a rollback to the savepoint is performed,
1258    /// the pages exceeding the database size at the start of the savepoint will be ignored.
1259    db_size: AtomicU32,
1260    /// WAL position to rewind to on `ROLLBACK TO`. Captured only under the
1261    /// write lock: eagerly if the savepoint is opened inside a write
1262    /// transaction, otherwise at write upgrade. `None` while the
1263    /// transaction has never held the write lock (no frames to rewind), or
1264    /// when the pager has no WAL.
1265    wal_pos: RwLock<Option<SavepointWalPos>>,
1266    /// Deferred FK counter value at the start of this savepoint.
1267    deferred_fk_violations: AtomicIsize,
1268}
1269
1270impl Savepoint {
1271    fn new(
1272        kind: SavepointKind,
1273        subjournal_offset: u64,
1274        db_size: u32,
1275        wal_pos: Option<SavepointWalPos>,
1276        deferred_fk_violations: isize,
1277    ) -> Self {
1278        Self {
1279            kind,
1280            start_offset: AtomicU64::new(subjournal_offset),
1281            write_offset: AtomicU64::new(subjournal_offset),
1282            page_bitmap: RwLock::new(RoaringBitmap::new()),
1283            db_size: AtomicU32::new(db_size),
1284            wal_pos: RwLock::new(wal_pos),
1285            deferred_fk_violations: AtomicIsize::new(deferred_fk_violations),
1286        }
1287    }
1288
1289    pub fn add_dirty_page(&self, page_num: u32) {
1290        self.page_bitmap.write().insert(page_num);
1291    }
1292
1293    pub fn has_dirty_page(&self, page_num: u32) -> bool {
1294        self.page_bitmap.read().contains(page_num)
1295    }
1296
1297    fn start_offset(&self) -> u64 {
1298        self.start_offset.load(Ordering::Acquire)
1299    }
1300
1301    fn write_offset(&self) -> u64 {
1302        self.write_offset.load(Ordering::Acquire)
1303    }
1304
1305    fn set_write_offset(&self, offset: u64) {
1306        self.write_offset.store(offset, Ordering::Release);
1307    }
1308
1309    fn snapshot(&self) -> SavepointSnapshot {
1310        SavepointSnapshot {
1311            kind: self.kind.clone(),
1312            start_offset: self.start_offset(),
1313            db_size: self.db_size.load(Ordering::Acquire),
1314            wal_pos: *self.wal_pos.read(),
1315            deferred_fk_violations: self.deferred_fk_violations.load(Ordering::Acquire),
1316        }
1317    }
1318
1319    fn from_snapshot(snapshot: SavepointSnapshot) -> Self {
1320        Self {
1321            kind: snapshot.kind,
1322            start_offset: AtomicU64::new(snapshot.start_offset),
1323            write_offset: AtomicU64::new(snapshot.start_offset),
1324            page_bitmap: RwLock::new(RoaringBitmap::new()),
1325            db_size: AtomicU32::new(snapshot.db_size),
1326            wal_pos: RwLock::new(snapshot.wal_pos),
1327            deferred_fk_violations: AtomicIsize::new(snapshot.deferred_fk_violations),
1328        }
1329    }
1330}
1331
1332/// The pager interface implements the persistence layer by providing access
1333/// to pages of the database file, including caching, concurrency control, and
1334/// transaction management.
1335pub struct Pager {
1336    /// Source of the database pages.
1337    pub db_file: Arc<dyn DatabaseStorage>,
1338    /// The write-ahead log (WAL) for the database.
1339    /// in-memory databases, ephemeral tables and ephemeral indexes do not have a WAL.
1340    pub(crate) wal: Option<Arc<dyn Wal>>,
1341    /// A page cache for the database.
1342    page_cache: Arc<RwLock<PageCache>>,
1343    /// Buffer pool for temporary data storage.
1344    pub buffer_pool: Arc<BufferPool>,
1345    /// I/O interface for input/output operations.
1346    pub io: Arc<dyn crate::io::IO>,
1347    /// Reads that have begun (disk IO issued, page allocated) but whose
1348    /// `cache_insert` has not yet succeeded because the cache was full and we
1349    /// yielded waiting for a spill to complete. The next call to
1350    /// `read_page_nonblock(idx)` reuses the stored `(page, disk_read)` pair
1351    /// instead of issuing a duplicate disk read.
1352    pending_reads: RwLock<HashMap<i64, PendingRead>>,
1353    #[cfg(clt_turso_tests)]
1354    spill_yield: SpillYieldHook,
1355    /// Dirty pages as a bitmap, naturally sorted by page number.
1356    dirty_pages: Arc<RwLock<RoaringBitmap>>,
1357    subjournal: RwLock<Option<Subjournal>>,
1358    savepoints: Arc<RwLock<Vec<Savepoint>>>,
1359    commit_info: RwLock<CommitInfo>,
1360    checkpoint_state: RwLock<CheckpointState>,
1361    syncing: Arc<AtomicBool>,
1362    auto_vacuum_mode: AtomicU8,
1363    /// Mutex for synchronizing database initialization to prevent race conditions
1364    init_lock: Arc<Mutex<()>>,
1365    /// The state of the current allocate page operation.
1366    allocate_page_state: RwLock<AllocatePageState>,
1367    /// The state of the current allocate page1 operation.
1368    allocate_page1_state: RwLock<AllocatePage1State>,
1369    /// Cache page_size and reserved_space at Pager init and reuse for subsequent
1370    /// `usable_space` calls. TODO: Invalidate reserved_space when we add the functionality
1371    /// to change it.
1372    pub(crate) page_size: AtomicU32,
1373    reserved_space: AtomicU16,
1374    /// Schema cookie cache.
1375    ///
1376    /// Note that schema cookie is 32-bits, but we use 64-bit field so we can
1377    /// represent case where value is not set.
1378    schema_cookie: AtomicU64,
1379    free_page_state: RwLock<FreePageState>,
1380    /// State machine for async cache spilling.
1381    spill_state: RwLock<SpillState>,
1382    /// State machine for async cacheflush operation.
1383    cacheflush_state: RwLock<CacheFlushState>,
1384    /// Maximum number of pages allowed in the database. Default is 1073741823 (SQLite default).
1385    max_page_count: AtomicU32,
1386    header_ref_state: RwLock<HeaderRefState>,
1387    #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1388    vacuum_state: RwLock<VacuumState>,
1389    pub(crate) io_ctx: RwLock<IOContext>,
1390    /// encryption is an opt-in feature. we will enable it only if the flag is passed
1391    enable_encryption: AtomicBool,
1392    /// In Memory Page 1 for Empty Dbs
1393    init_page_1: Arc<ArcSwapOption<Page>>,
1394    /// Sync type for durability. FullFsync uses F_FULLFSYNC on macOS (PRAGMA fullfsync).
1395    /// Only stored on Apple platforms; on others, always returns Fsync.
1396    #[cfg(target_vendor = "apple")]
1397    sync_type: AtomicFileSyncType,
1398    /// Live BTreeCursors on this pager, bucketed by btree root page.
1399    /// Counterpart of SQLite's BtShared.pCursor list; bucketing per root
1400    /// supplies the BTCF_Multiple fast path (btree.c:9348).
1401    pub(crate) cursor_registry: Mutex<rustc_hash::FxHashMap<i64, Vec<RegisteredCursor>>>,
1402}
1403
1404/// Raw fat pointer to a registered cursor.
1405///
1406/// # Safety
1407/// Dereferencing requires that no other reference to the referent cursor
1408/// is live at the same time. The registry Mutex serializes registry
1409/// mutation but not access to the cursors themselves; that exclusion comes
1410/// from the execution model — a Connection runs one statement at a time,
1411/// and a statement's cursors are touched only by its executor thread.
1412///
1413/// Identity compares the data half of the pointer only: codegen may emit
1414/// multiple vtable pointers for the same trait/type pair across
1415/// translation units, so vtable comparison can spuriously fail.
1416#[derive(Clone, Copy)]
1417pub(crate) struct RegisteredCursor(std::ptr::NonNull<dyn crate::storage::btree::CursorTrait>);
1418
1419impl RegisteredCursor {
1420    pub(crate) fn for_cursor(cursor: &dyn crate::storage::btree::CursorTrait) -> Self {
1421        Self(std::ptr::NonNull::from(cursor))
1422    }
1423
1424    /// # Safety
1425    /// See the type-level doc.
1426    #[allow(clippy::mut_from_ref)]
1427    pub(crate) unsafe fn as_mut(&self) -> &mut dyn crate::storage::btree::CursorTrait {
1428        unsafe { self.0.as_ptr().as_mut().unwrap() }
1429    }
1430}
1431
1432impl PartialEq for RegisteredCursor {
1433    fn eq(&self, other: &Self) -> bool {
1434        self.0.as_ptr() as *const () == other.0.as_ptr() as *const ()
1435    }
1436}
1437
1438impl Eq for RegisteredCursor {}
1439
1440unsafe impl Send for RegisteredCursor {}
1441unsafe impl Sync for RegisteredCursor {}
1442
1443assert_send_sync!(Pager);
1444
1445/// State for a `read_page_nonblock` call that has issued its disk read but has
1446/// not yet been able to insert the page into the cache (cache full, spill in
1447/// flight). Stored in `Pager::pending_reads` so the next re-entry can resume
1448/// without issuing a duplicate disk read.
1449#[derive(Clone)]
1450struct PendingRead {
1451    page: PageRef,
1452    /// `None` if the page was satisfied from WAL/cache shortcut and no
1453    /// disk read completion needs to be surfaced to the caller.
1454    disk_read: Option<Completion>,
1455}
1456
1457/// Test-only deterministic spill-yield injector for `Pager::read_page`. When
1458/// armed, the next matching call (after `skip` ignored matches) returns
1459/// `IO(yield)` once, then disarms itself.
1460#[cfg(clt_turso_tests)]
1461struct SpillYieldHook {
1462    /// `-1` = disarmed; otherwise the `page_idx` to fire on.
1463    target: std::sync::atomic::AtomicI64,
1464    /// Number of matching calls to ignore before firing.
1465    skip: std::sync::atomic::AtomicUsize,
1466}
1467
1468#[cfg(clt_turso_tests)]
1469impl SpillYieldHook {
1470    const fn new() -> Self {
1471        Self {
1472            target: std::sync::atomic::AtomicI64::new(-1),
1473            skip: std::sync::atomic::AtomicUsize::new(0),
1474        }
1475    }
1476
1477    fn arm(&self, page_id: i64, skip: usize) {
1478        use std::sync::atomic::Ordering::Relaxed;
1479        self.skip.store(skip, Relaxed);
1480        self.target.store(page_id, Relaxed);
1481    }
1482
1483    /// Returns true exactly once per arming, when the (`skip + 1`)th call for
1484    /// the armed page id arrives. Internal load-then-store is fine because
1485    /// tests using this hook are single-threaded.
1486    fn should_yield_for(&self, page_idx: i64) -> bool {
1487        use std::sync::atomic::Ordering::Relaxed;
1488        if self.target.load(Relaxed) != page_idx {
1489            return false;
1490        }
1491        if self.skip.load(Relaxed) == 0 {
1492            self.target.store(-1, Relaxed);
1493            true
1494        } else {
1495            self.skip.fetch_sub(1, Relaxed);
1496            false
1497        }
1498    }
1499}
1500
1501#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1502pub struct VacuumState {
1503    /// State machine for [Pager::ptrmap_get]
1504    ptrmap_get_state: PtrMapGetState,
1505    /// State machine for [Pager::ptrmap_put]
1506    ptrmap_put_state: PtrMapPutState,
1507    btree_create_vacuum_full_state: BtreeCreateVacuumFullState,
1508}
1509
1510#[derive(Debug, Clone)]
1511enum AllocatePageState {
1512    Start,
1513    /// Search the trunk page for an available free list leaf.
1514    /// If none are found, there are two options:
1515    /// - If there are no more trunk pages, the freelist is empty, so allocate a new page.
1516    /// - If there are more trunk pages, use the current first trunk page as the new allocation,
1517    ///   and set the next trunk page as the database's "first freelist trunk page".
1518    SearchAvailableFreeListLeaf {
1519        trunk_page: PageRef,
1520    },
1521    /// If a freelist leaf is found, reuse it for the page allocation and remove it from the trunk page.
1522    ReuseFreelistLeaf {
1523        trunk_page: PageRef,
1524        leaf_page: PageRef,
1525        number_of_freelist_leaves: u32,
1526    },
1527    /// If a suitable freelist leaf is not found, allocate an entirely new page.
1528    AllocateNewPage {
1529        current_db_size: u32,
1530    },
1531}
1532
1533#[derive(Clone)]
1534enum AllocatePage1State {
1535    Start,
1536    Writing { page: PageRef },
1537    Done,
1538}
1539
1540#[derive(Debug, Clone)]
1541enum FreePageState {
1542    Start,
1543    AddToTrunk { page: Arc<Page> },
1544    NewTrunk { page: Arc<Page> },
1545}
1546
1547/// State machine for async cache spilling.
1548/// Tracks progress of writing dirty pages to WAL or disk.
1549#[derive(Debug, Default, Clone)]
1550enum SpillState {
1551    #[default]
1552    /// No spill operation in progress
1553    Idle,
1554    /// Lazily initializing the WAL header before the first spill write.
1555    /// Waiting for the header write (and possible truncate) to complete.
1556    /// The pinned pages destined for spilling are carried across the yield.
1557    PreparingWalStart {
1558        pages: Vec<PinGuard>,
1559        completion: Completion,
1560    },
1561    /// WAL header written; waiting for the fsync that marks the WAL
1562    /// initialized before we append spill frames.
1563    PreparingWalFinish {
1564        pages: Vec<PinGuard>,
1565        completion: Completion,
1566    },
1567    /// WAL spill in progress, waiting for write completions
1568    WritingToWal {
1569        /// Pinned pages being spilled
1570        pages: Vec<PinGuard>,
1571        /// Completions to wait for
1572        completions: Vec<Completion>,
1573    },
1574    /// Writing ephemeral tables pages directly to disk
1575    WritingToDisk {
1576        /// Pages being spilled
1577        pages: Vec<PinGuard>,
1578        /// Completions to wait for
1579        completions: Vec<Completion>,
1580    },
1581}
1582enum CacheFlushStep {
1583    /// Yield to caller with pending I/O, resume with given phase
1584    Yield(CacheFlushState, IOCompletions),
1585    /// Continue immediately to next phase (no I/O wait)
1586    Continue(CacheFlushState),
1587    /// Flush complete, return accumulated completions
1588    Done(Vec<Completion>),
1589}
1590
1591#[derive(Default)]
1592pub enum CacheFlushState {
1593    #[default]
1594    Init,
1595    WalPrepareStart {
1596        dirty_ids: Vec<usize>,
1597        completion: Completion,
1598    },
1599    WalPrepareFinish {
1600        dirty_ids: Vec<usize>,
1601        completion: Completion,
1602    },
1603    Collecting(CollectingState),
1604    WaitingForRead {
1605        state: CollectingState,
1606        page_id: usize,
1607        page: PageRef,
1608        completion: Completion,
1609    },
1610}
1611
1612#[derive(Default)]
1613pub struct CollectingState {
1614    pub dirty_ids: Vec<usize>,
1615    pub current_idx: usize,
1616    pub collected_pages: Vec<PageRef>,
1617    pub completions: Vec<Completion>,
1618}
1619
1620impl Pager {
1621    pub fn new(
1622        db_file: Arc<dyn DatabaseStorage>,
1623        wal: Option<Arc<dyn Wal>>,
1624        io: Arc<dyn crate::io::IO>,
1625        page_cache: PageCache,
1626        buffer_pool: Arc<BufferPool>,
1627        init_lock: Arc<Mutex<()>>,
1628        init_page_1: Arc<ArcSwapOption<Page>>,
1629    ) -> Result<Self> {
1630        let allocate_page1_state = if init_page_1.load().is_some() {
1631            RwLock::new(AllocatePage1State::Start)
1632        } else {
1633            RwLock::new(AllocatePage1State::Done)
1634        };
1635        Ok(Self {
1636            db_file,
1637            wal,
1638            page_cache: Arc::new(RwLock::new(page_cache)),
1639            io,
1640            pending_reads: RwLock::new(HashMap::new()),
1641            #[cfg(clt_turso_tests)]
1642            spill_yield: SpillYieldHook::new(),
1643            dirty_pages: Arc::new(RwLock::new(RoaringBitmap::new())),
1644            subjournal: RwLock::new(None),
1645            savepoints: Arc::new(RwLock::new(Vec::new())),
1646            commit_info: RwLock::new(CommitInfo {
1647                completions: Vec::new(),
1648                completion_group: None,
1649                state: CommitState::PrepareWal,
1650                collected_pages: Vec::new(),
1651                prepared_frames: Vec::new(),
1652                page_sources: Vec::new(),
1653                page_source_cursor: 0,
1654            }),
1655            syncing: Arc::new(AtomicBool::new(false)),
1656            checkpoint_state: RwLock::new(CheckpointState::default()),
1657            buffer_pool,
1658            auto_vacuum_mode: AtomicU8::new(AutoVacuumMode::None.into()),
1659            init_lock,
1660            allocate_page1_state,
1661            page_size: AtomicU32::new(0), // 0 means not set
1662            reserved_space: AtomicU16::new(RESERVED_SPACE_NOT_SET),
1663            schema_cookie: AtomicU64::new(Self::SCHEMA_COOKIE_NOT_SET),
1664            free_page_state: RwLock::new(FreePageState::Start),
1665            spill_state: RwLock::new(SpillState::Idle),
1666            cacheflush_state: RwLock::new(CacheFlushState::default()),
1667            allocate_page_state: RwLock::new(AllocatePageState::Start),
1668            max_page_count: AtomicU32::new(DEFAULT_MAX_PAGE_COUNT),
1669            header_ref_state: RwLock::new(HeaderRefState::Start),
1670            #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
1671            vacuum_state: RwLock::new(VacuumState {
1672                ptrmap_get_state: PtrMapGetState::Start,
1673                ptrmap_put_state: PtrMapPutState::Start,
1674                btree_create_vacuum_full_state: BtreeCreateVacuumFullState::Start,
1675            }),
1676            io_ctx: RwLock::new(IOContext::default()),
1677            enable_encryption: AtomicBool::new(false),
1678            init_page_1,
1679            #[cfg(target_vendor = "apple")]
1680            sync_type: AtomicFileSyncType::new(FileSyncType::Fsync),
1681            cursor_registry: Mutex::new(rustc_hash::FxHashMap::default()),
1682        })
1683    }
1684
1685    /// Add a cursor to the registry. Called from Cursor::new_btree once the
1686    /// cursor lives in its final heap location; BTreeCursor::drop unregisters.
1687    pub(crate) fn register_cursor(&self, cursor: &dyn crate::storage::btree::CursorTrait) {
1688        let root = cursor.root_page();
1689        let mut registry = self.cursor_registry.lock();
1690        let bucket = registry.entry(root).or_default();
1691        bucket.push(RegisteredCursor::for_cursor(cursor));
1692        // Set BTCF_Multiple on every cursor in the bucket. Idempotent for
1693        // existing peers; we don't bother branching on the 1→2 transition.
1694        if bucket.len() >= 2 {
1695            for &peer in bucket.iter() {
1696                // SAFETY: see RegisteredCursor's invariant.
1697                unsafe { peer.as_mut().set_has_peers_for_external_writes(true) };
1698            }
1699        }
1700    }
1701
1702    pub(crate) fn unregister_cursor(&self, cursor: &dyn crate::storage::btree::CursorTrait) {
1703        let target = RegisteredCursor::for_cursor(cursor);
1704        let root = cursor.root_page();
1705        let mut registry = self.cursor_registry.lock();
1706        if let Some(bucket) = registry.get_mut(&root) {
1707            if let Some(idx) = bucket.iter().position(|c| *c == target) {
1708                bucket.swap_remove(idx);
1709            }
1710            // 2→1: clear BTCF_Multiple on the survivor.
1711            if bucket.len() == 1 {
1712                let surviving = bucket[0];
1713                // SAFETY: see RegisteredCursor's invariant.
1714                unsafe { surviving.as_mut().set_has_peers_for_external_writes(false) };
1715            }
1716            if bucket.is_empty() {
1717                registry.remove(&root);
1718            }
1719        }
1720    }
1721
1722    /// Snapshot all peers of `except` on the same btree root. Snapshotting
1723    /// under the lock lets the caller iterate (and yield IO) without
1724    /// blocking concurrent cursor open/close on the registry.
1725    pub(crate) fn snapshot_peers_for_root(
1726        &self,
1727        except: &dyn crate::storage::btree::CursorTrait,
1728    ) -> smallvec::SmallVec<[RegisteredCursor; 4]> {
1729        let except_handle = RegisteredCursor::for_cursor(except);
1730        let except_root = except.root_page();
1731        let registry = self.cursor_registry.lock();
1732        let Some(bucket) = registry.get(&except_root) else {
1733            return smallvec::SmallVec::new();
1734        };
1735        if bucket.len() <= 1 {
1736            return smallvec::SmallVec::new();
1737        }
1738        bucket
1739            .iter()
1740            .copied()
1741            .filter(|c| *c != except_handle)
1742            .collect()
1743    }
1744
1745    /// Invalidate every cursor's page stack. Called from rollback — pinned
1746    /// pages may now hold pre-rollback bytes (cf. saveAllCursors in
1747    /// sqlite3BtreeRollback, btree.c:4485).
1748    pub(crate) fn invalidate_all_cursors(&self) {
1749        let snapshot: smallvec::SmallVec<[RegisteredCursor; 8]> = {
1750            let registry = self.cursor_registry.lock();
1751            registry.values().flat_map(|b| b.iter().copied()).collect()
1752        };
1753        for peer in snapshot {
1754            // SAFETY: see RegisteredCursor's invariant.
1755            unsafe { peer.as_mut().invalidate_btree_cache() };
1756        }
1757    }
1758
1759    /// Invalidate the page stacks of every peer on `except`'s btree. Used
1760    /// by clear_btree / btree_destroy where every page is freed; saving
1761    /// positions would just stash keys that no longer exist.
1762    pub(crate) fn invalidate_peer_cursors(&self, except: &dyn crate::storage::btree::CursorTrait) {
1763        let peers = self.snapshot_peers_for_root(except);
1764        for peer in peers {
1765            // SAFETY: see RegisteredCursor's invariant.
1766            unsafe { peer.as_mut().invalidate_btree_cache() };
1767        }
1768    }
1769
1770    /// Get the sync type setting.
1771    /// On non-Apple platforms, always returns Fsync (compile-time constant).
1772    #[cfg(target_vendor = "apple")]
1773    #[inline]
1774    pub fn get_sync_type(&self) -> FileSyncType {
1775        self.sync_type.get()
1776    }
1777
1778    /// Get the sync type setting.
1779    /// On non-Apple platforms, always returns Fsync (compile-time constant).
1780    #[cfg(not(target_vendor = "apple"))]
1781    #[inline]
1782    pub fn get_sync_type(&self) -> FileSyncType {
1783        FileSyncType::Fsync
1784    }
1785
1786    /// Set the sync type (for PRAGMA fullfsync). Only effective on Apple platforms.
1787    #[cfg(target_vendor = "apple")]
1788    pub fn set_sync_type(&self, value: FileSyncType) {
1789        self.sync_type.set(value);
1790    }
1791
1792    /// Set the sync type. No-op on non-Apple platforms.
1793    #[cfg(not(target_vendor = "apple"))]
1794    pub fn set_sync_type(&self, _value: FileSyncType) {
1795        // No-op: FullFsync only has effect on Apple platforms
1796    }
1797
1798    pub fn init_page_1(&self) -> Arc<ArcSwapOption<Page>> {
1799        self.init_page_1.clone()
1800    }
1801
1802    /// Read page 1 (the database header page) using the header_ref_state state machine.
1803    /// Used by HeaderRef and HeaderRefMut to avoid duplicating the page-loading logic.
1804    fn read_header_page(&self) -> Result<IOResult<PageRef>> {
1805        loop {
1806            let state = self.header_ref_state.read().clone();
1807            tracing::trace!("read_header_page - {:?}", state);
1808            match state {
1809                HeaderRefState::Start => {
1810                    // If db is not initialized, return the in-memory page
1811                    if let Some(page1) = self.init_page_1.load_full() {
1812                        return Ok(IOResult::Done(page1));
1813                    }
1814
1815                    // On spill `return_if_io!` propagates IO up unchanged so
1816                    // re-entry resumes here via the pager's `pending_reads`
1817                    // memoization (no duplicate disk read).
1818                    let (page, c) = return_if_io!(self.read_page(DatabaseHeader::PAGE_ID as i64));
1819                    *self.header_ref_state.write() = HeaderRefState::CreateHeader {
1820                        page,
1821                        completion: c.clone(),
1822                    };
1823                    if let Some(c) = c {
1824                        io_yield_one!(c);
1825                    }
1826                }
1827                HeaderRefState::CreateHeader { page, completion } => {
1828                    // Check if the read failed (e.g., due to checksum/decryption error)
1829                    if let Some(ref c) = completion {
1830                        if let Some(err) = c.get_error() {
1831                            *self.header_ref_state.write() = HeaderRefState::Start;
1832                            return Err(err.into());
1833                        }
1834                    }
1835                    turso_assert!(page.is_loaded(), "page should be loaded");
1836                    turso_assert!(
1837                        page.get().id == DatabaseHeader::PAGE_ID,
1838                        "incorrect header page id"
1839                    );
1840                    *self.header_ref_state.write() = HeaderRefState::Start;
1841                    return Ok(IOResult::Done(page));
1842                }
1843            }
1844        }
1845    }
1846
1847    /// Set whether cache spilling is enabled.
1848    pub fn set_spill_enabled(&self, enabled: bool) {
1849        self.page_cache.write().set_spill_enabled(enabled);
1850    }
1851    /// Get whether cache spilling is enabled.
1852    pub fn get_spill_enabled(&self) -> bool {
1853        self.page_cache.read().is_spill_enabled()
1854    }
1855
1856    /// Open the subjournal if not yet open.
1857    /// The subjournal is a file that is used to store the "before images" of pages for the
1858    /// current savepoint. If the savepoint is rolled back, the pages can be restored from the subjournal.
1859    ///
1860    /// Currently uses MemoryIO, but should eventually be backed by temporary on-disk files.
1861    pub fn open_subjournal(&self) -> Result<()> {
1862        if self.subjournal.read().is_some() {
1863            return Ok(());
1864        }
1865        use crate::MemoryIO;
1866
1867        let db_file_io = Arc::new(MemoryIO::new());
1868        let file = db_file_io.open_file("subjournal", OpenFlags::Create, false)?;
1869        let db_file = Subjournal::new(file);
1870        *self.subjournal.write() = Some(db_file);
1871        Ok(())
1872    }
1873
1874    /// Write page to subjournal if the current savepoint does not currently
1875    /// contain an an entry for it. In case of a statement-level rollback,
1876    /// the page image can be restored from the subjournal.
1877    ///
1878    /// A buffer of length page_size + 4 bytes is allocated and the page id
1879    /// is written to the beginning of the buffer. The rest of the buffer is filled with the page contents.
1880    pub fn subjournal_page_if_required(&self, page: &Page) -> Result<()> {
1881        if self.subjournal.read().is_none() {
1882            return Ok(());
1883        }
1884        let write_offset = {
1885            let savepoints = self.savepoints.read();
1886            let Some(cur_savepoint) = savepoints.last() else {
1887                return Ok(());
1888            };
1889            // Skip subjournaling for pages that didn't exist when the savepoint was opened.
1890            // New pages (allocated during this statement) can be "rolled back" by simply
1891            // truncating back to the original db_size. This matches SQLite's subjRequiresPage()
1892            // which checks: p->nOrig >= pgno.
1893            let page_id_u32 = page.get().id as u32;
1894            if page_id_u32 > cur_savepoint.db_size.load(Ordering::Acquire) {
1895                return Ok(());
1896            }
1897            if cur_savepoint.has_dirty_page(page_id_u32) {
1898                return Ok(());
1899            }
1900            cur_savepoint.write_offset.load(Ordering::SeqCst)
1901        };
1902        let page_id = page.get().id;
1903        let page_size = self.page_size.load(Ordering::SeqCst) as usize;
1904        let buffer = {
1905            let page_id = page.get().id as u32;
1906            let contents = page.get_contents();
1907            let buffer = self.buffer_pool.allocate(page_size + 4);
1908            let contents_buffer = contents.as_ptr();
1909            turso_assert!(
1910                contents_buffer.len() == page_size,
1911                "contents buffer length should be equal to page size"
1912            );
1913
1914            buffer.as_mut_slice()[0..4].copy_from_slice(&page_id.to_be_bytes());
1915            buffer.as_mut_slice()[4..4 + page_size].copy_from_slice(contents_buffer);
1916
1917            Arc::new(buffer)
1918        };
1919
1920        let savepoints = self.savepoints.clone();
1921
1922        let write_complete = {
1923            let buf_copy = buffer.clone();
1924            Box::new(move |res: Result<i32, CompletionError>| {
1925                let Ok(bytes_written) = res else {
1926                    return;
1927                };
1928                let buf_copy = buf_copy.clone();
1929                let buf_len = buf_copy.len();
1930
1931                turso_assert!(
1932                    bytes_written == buf_len as i32,
1933                    "wrote({bytes_written}) != expected({buf_len})"
1934                );
1935
1936                let savepoints = savepoints.read();
1937                let cur_savepoint = savepoints.last().unwrap();
1938                cur_savepoint.add_dirty_page(page_id as u32);
1939                cur_savepoint
1940                    .write_offset
1941                    .fetch_add(page_size as u64 + 4, Ordering::SeqCst);
1942            })
1943        };
1944        let c = Completion::new_write(write_complete);
1945
1946        let subjournal = self.subjournal.read();
1947        let subjournal = subjournal.as_ref().unwrap();
1948
1949        let c = subjournal.write_page(write_offset, page_size, buffer, c)?;
1950        turso_assert!(c.succeeded(), "memory IO should complete immediately");
1951        Ok(())
1952    }
1953
1954    /// try to "acquire" ownership on the subjournal of the connection-scoped pager
1955    /// if another statement owns the subjournal - return Busy error and let the caller retry attempt later
1956    pub fn try_use_subjournal(&self) -> Result<()> {
1957        let subjournal = self.subjournal.read();
1958        let subjournal = subjournal.as_ref().expect("subjournal must be opened");
1959        subjournal.try_use()
1960    }
1961
1962    /// release ownership of the subjournal
1963    /// caller must guarantee that [Self::stop_use_subjournal] is called only after successful call to the [Self::try_use_subjournal]
1964    pub fn stop_use_subjournal(&self) {
1965        let subjournal = self.subjournal.read();
1966        let subjournal = subjournal.as_ref().expect("subjournal must be opened");
1967        subjournal.stop_use()
1968    }
1969
1970    /// check if subjournal is in use for some statement
1971    pub fn subjournal_in_use(&self) -> bool {
1972        let subjournal = self.subjournal.read();
1973        let Some(subjournal) = subjournal.as_ref() else {
1974            return false;
1975        };
1976        subjournal.in_use()
1977    }
1978
1979    pub fn open_savepoint(&self, db_size: u32) -> Result<()> {
1980        self.open_savepoint_with_kind(SavepointKind::Statement, db_size, 0)
1981    }
1982
1983    /// Release i.e. commit the current savepoint. This basically just means removing it.
1984    pub fn release_savepoint(&self) -> Result<()> {
1985        let mut savepoints = self.savepoints.write();
1986        if !matches!(
1987            savepoints.last().map(|savepoint| &savepoint.kind),
1988            Some(SavepointKind::Statement)
1989        ) {
1990            return Ok(());
1991        }
1992        let savepoint = savepoints.pop().expect("savepoint must exist");
1993        if let Some(parent) = savepoints.last() {
1994            parent.set_write_offset(savepoint.write_offset());
1995        } else {
1996            let subjournal = self.subjournal.read();
1997            let Some(subjournal) = subjournal.as_ref() else {
1998                return Ok(());
1999            };
2000            let c = subjournal.truncate(0)?;
2001            turso_assert!(c.succeeded(), "memory IO should complete immediately");
2002        }
2003        Ok(())
2004    }
2005
2006    /// Opens a named savepoint and captures rollback metadata for the current transaction state.
2007    ///
2008    /// If `starts_transaction` is true, releasing this savepoint at the root depth commits the
2009    /// transaction.
2010    pub fn open_named_savepoint(
2011        &self,
2012        name: String,
2013        db_size: u32,
2014        starts_transaction: bool,
2015        deferred_fk_violations: isize,
2016    ) -> Result<()> {
2017        self.open_savepoint_with_kind(
2018            SavepointKind::Named {
2019                name,
2020                starts_transaction,
2021            },
2022            db_size,
2023            deferred_fk_violations,
2024        )
2025    }
2026
2027    /// Releases the newest matching named savepoint and all nested savepoints opened after it.
2028    pub fn release_named_savepoint(&self, name: &str) -> Result<SavepointResult> {
2029        let mut savepoints = self.savepoints.write();
2030        let Some(target_idx) = savepoints.iter().rposition(|savepoint| {
2031            matches!(
2032                savepoint.kind,
2033                SavepointKind::Named {
2034                    name: ref savepoint_name,
2035                    ..
2036                } if savepoint_name == name
2037            )
2038        }) else {
2039            return Ok(SavepointResult::NotFound);
2040        };
2041
2042        let result = if matches!(
2043            savepoints[target_idx].kind,
2044            SavepointKind::Named {
2045                starts_transaction: true,
2046                ..
2047            }
2048        ) && target_idx == 0
2049        {
2050            SavepointResult::Commit
2051        } else {
2052            SavepointResult::Release
2053        };
2054        if matches!(result, SavepointResult::Commit) {
2055            // Defer mutation until transaction commit succeeds. If commit fails
2056            // (e.g. deferred FK violation), savepoints must remain intact.
2057            return Ok(result);
2058        }
2059        let journal_end_offset = savepoints
2060            .last()
2061            .map(|savepoint| savepoint.write_offset())
2062            .unwrap_or(0);
2063
2064        savepoints.truncate(target_idx);
2065
2066        if let Some(parent) = savepoints.last() {
2067            parent.set_write_offset(journal_end_offset);
2068        } else {
2069            let subjournal = self.subjournal.read();
2070            let Some(subjournal) = subjournal.as_ref() else {
2071                return Ok(result);
2072            };
2073            let c = subjournal.truncate(0)?;
2074            assert!(c.succeeded(), "memory IO should complete immediately");
2075        }
2076
2077        Ok(result)
2078    }
2079
2080    pub fn clear_savepoints(&self) -> Result<()> {
2081        *self.savepoints.write() = Vec::new();
2082        let subjournal = self.subjournal.read();
2083        let Some(subjournal) = subjournal.as_ref() else {
2084            return Ok(());
2085        };
2086        let c = subjournal.truncate(0)?;
2087        turso_assert!(c.succeeded(), "memory IO should complete immediately");
2088        Ok(())
2089    }
2090
2091    /// Rollback to the newest savepoint. This basically just means reading the subjournal from the start offset
2092    /// of the savepoint to the end of the subjournal and restoring the page images to the page cache.
2093    pub fn rollback_to_newest_savepoint(&self) -> Result<bool> {
2094        let mut savepoints = self.savepoints.write();
2095        if !matches!(
2096            savepoints.last().map(|savepoint| &savepoint.kind),
2097            Some(SavepointKind::Statement)
2098        ) {
2099            return Ok(false);
2100        }
2101        let savepoint = savepoints.pop().expect("savepoint must exist");
2102        let journal_end_offset = savepoint.write_offset();
2103        let savepoint = savepoint.snapshot();
2104
2105        self.rollback_to_snapshot(&savepoint, journal_end_offset)?;
2106
2107        if let Some(parent) = savepoints.last() {
2108            parent.set_write_offset(savepoint.start_offset);
2109        }
2110
2111        Ok(true)
2112    }
2113
2114    /// Rollback to the newest matching named savepoint while keeping the named savepoint active.
2115    ///
2116    /// Returns deferred FK counter snapshot for the rolled-back savepoint.
2117    pub fn rollback_to_named_savepoint(&self, name: &str) -> Result<Option<isize>> {
2118        let target = {
2119            let savepoints = self.savepoints.read();
2120            let Some(target_idx) = savepoints.iter().rposition(|savepoint| {
2121                matches!(
2122                    savepoint.kind,
2123                    SavepointKind::Named {
2124                        name: ref savepoint_name,
2125                        ..
2126                    } if savepoint_name == name
2127                )
2128            }) else {
2129                return Ok(None);
2130            };
2131            let journal_end_offset = savepoints
2132                .last()
2133                .map(|savepoint| savepoint.write_offset())
2134                .unwrap_or_else(|| savepoints[target_idx].write_offset());
2135            (
2136                target_idx,
2137                savepoints[target_idx].snapshot(),
2138                journal_end_offset,
2139            )
2140        };
2141
2142        self.rollback_to_snapshot(&target.1, target.2)?;
2143
2144        let mut savepoints = self.savepoints.write();
2145        let deferred_fk_violations = target.1.deferred_fk_violations;
2146        savepoints.truncate(target.0);
2147        if let Some(parent) = savepoints.last() {
2148            parent.set_write_offset(target.1.start_offset);
2149        }
2150        savepoints.push(Savepoint::from_snapshot(target.1));
2151
2152        Ok(Some(deferred_fk_violations))
2153    }
2154
2155    fn open_savepoint_with_kind(
2156        &self,
2157        kind: SavepointKind,
2158        db_size: u32,
2159        deferred_fk_violations: isize,
2160    ) -> Result<()> {
2161        let subjournal_offset = self
2162            .savepoints
2163            .read()
2164            .last()
2165            .map(|savepoint| savepoint.write_offset())
2166            .unwrap_or(0);
2167        let wal_pos = self
2168            .wal
2169            .as_ref()
2170            .filter(|wal| wal.holds_write_lock())
2171            .map(|wal| SavepointWalPos {
2172                max_frame: wal.get_max_frame(),
2173                checksum: wal.get_last_checksum(),
2174                checkpoint_seq: wal.get_checkpoint_seq(),
2175            });
2176        let savepoint = Savepoint::new(
2177            kind,
2178            subjournal_offset,
2179            db_size,
2180            wal_pos,
2181            deferred_fk_violations,
2182        );
2183        self.savepoints.write().push(savepoint);
2184        Ok(())
2185    }
2186
2187    #[aristo::intent(
2188        "Rolling back to a savepoint rewinds the database shape and the page bytes to \
2189         one consistent snapshot, split at the savepoint's database size. Each page at \
2190         or below that size that was modified during the savepoint is restored from its \
2191         pre-savepoint image, kept dirty, and re-inserted into the cache. Pages left \
2192         untouched during the savepoint keep their existing content. Every page beyond \
2193         that size is removed from both the dirty set and the cache. No page reachable \
2194         by the restored header page count or by a restored btree pointer is left as an \
2195         unwritten zero slot. Restoring the pre-images and discarding the beyond-boundary \
2196         pages must happen together; dropping either half leaves a live page pointing at \
2197         zeroed bytes, which the next read rejects as an invalid page type.",
2198        verify = "neural",
2199        id = "savepoint_rollback_shape_and_bytes_consistent"
2200    )]
2201    fn rollback_to_snapshot(
2202        &self,
2203        savepoint: &SavepointSnapshot,
2204        journal_end_offset: u64,
2205    ) -> Result<()> {
2206        self.reset_internal_states();
2207
2208        let subjournal = self.subjournal.read();
2209        let Some(subjournal) = subjournal.as_ref() else {
2210            return Ok(());
2211        };
2212
2213        let journal_start_offset = savepoint.start_offset;
2214        let db_size = savepoint.db_size;
2215
2216        let mut rollback_bitset = RoaringBitmap::new();
2217        let mut current_offset = journal_start_offset;
2218        let page_size = self.page_size.load(Ordering::SeqCst) as u64;
2219        let mut dirty_pages = self.dirty_pages.write();
2220
2221        while current_offset < journal_end_offset {
2222            let page_id_buffer = Arc::new(self.buffer_pool.allocate(4));
2223            let c = subjournal.read_page_number(current_offset, page_id_buffer.clone())?;
2224            turso_assert!(c.succeeded(), "memory IO should complete immediately");
2225            let page_id = u32::from_be_bytes(page_id_buffer.as_slice()[0..4].try_into().unwrap());
2226            current_offset += 4;
2227
2228            if rollback_bitset.contains(page_id) {
2229                current_offset += page_size;
2230                continue;
2231            }
2232            if page_id > db_size {
2233                current_offset += page_size;
2234                continue;
2235            }
2236
2237            let page_buffer = Arc::new(self.buffer_pool.allocate(page_size as usize));
2238            let page = Arc::new(Page::new(page_id as i64));
2239            let c = subjournal.read_page(
2240                current_offset,
2241                page_buffer,
2242                page.clone(),
2243                page_size as usize,
2244            )?;
2245            turso_assert!(c.succeeded(), "memory IO should complete immediately");
2246            current_offset += page_size;
2247            rollback_bitset.insert(page_id);
2248            // The restored image is the transaction-visible state at the
2249            // savepoint, not necessarily durable state. Keep it dirty so cache
2250            // eviction cannot drop uncommitted changes that predate the
2251            // rolled-back savepoint/statement.
2252            page.set_dirty();
2253            dirty_pages.insert(page_id);
2254            self.force_upsert_page_in_cache(page_id as usize, page)?;
2255        }
2256
2257        let truncate_completion = subjournal.truncate(journal_start_offset)?;
2258        turso_assert!(
2259            truncate_completion.succeeded(),
2260            "memory IO should complete immediately"
2261        );
2262
2263        // Discard all dirty pages allocated after the savepoint. These pages
2264        // are never subjournaled (see subjournal_page_if_required), so the loop
2265        // above won't encounter them. We must clean them from dirty_pages before
2266        // truncating the cache, or phantom dirty entries survive into commit.
2267        {
2268            let mut cache = self.page_cache.write();
2269            for page_id in dirty_pages.iter().filter(|&id| id > db_size) {
2270                if let Some(page) = cache.get(&PageCacheKey::new(page_id as usize))? {
2271                    page.clear_dirty();
2272                    page.try_unpin();
2273                }
2274            }
2275            dirty_pages.remove_range((db_size + 1)..);
2276            cache.truncate(db_size as usize)?;
2277        }
2278
2279        // No WAL position: the transaction never upgraded to a write
2280        // transaction, so there are no frames to rewind.
2281        if let (Some(wal), Some(wal_pos)) = (&self.wal, savepoint.wal_pos) {
2282            wal.rollback(Some(RollbackTo {
2283                frame: wal_pos.max_frame,
2284                checksum: wal_pos.checksum,
2285                checkpoint_seq: wal_pos.checkpoint_seq,
2286            }));
2287            self.page_cache
2288                .write()
2289                .delete_clean_pages_after_wal_frame(wal_pos.max_frame)
2290                .map_err(|e| {
2291                    LimboError::InternalError(format!(
2292                        "failed to invalidate rolled-back WAL pages: {e:?}"
2293                    ))
2294                })?;
2295        }
2296
2297        // saveAllCursors at sqlite3BtreeSavepoint (btree.c:4580).
2298        self.invalidate_all_cursors();
2299
2300        Ok(())
2301    }
2302
2303    #[cfg(clt_turso_feature = "test_helper")]
2304    pub fn get_pending_byte() -> u32 {
2305        PENDING_BYTE.load(Ordering::Relaxed)
2306    }
2307
2308    #[cfg(clt_turso_feature = "test_helper")]
2309    /// Used in testing to allow for pending byte pages in smaller dbs
2310    pub fn set_pending_byte(val: u32) {
2311        PENDING_BYTE.store(val, Ordering::Relaxed);
2312    }
2313
2314    #[cfg(not(clt_turso_feature = "test_helper"))]
2315    pub const fn get_pending_byte() -> u32 {
2316        PENDING_BYTE
2317    }
2318
2319    /// From SQLITE: https://github.com/sqlite/sqlite/blob/7e38287da43ea3b661da3d8c1f431aa907d648c9/src/btreeInt.h#L608 \
2320    /// The database page the [PENDING_BYTE] occupies. This page is never used.
2321    pub fn pending_byte_page_id(&self) -> Option<u32> {
2322        // PENDING_BYTE_PAGE(pBt)  ((Pgno)((PENDING_BYTE/((pBt)->pageSize))+1))
2323        let page_size = self.page_size.load(Ordering::SeqCst);
2324        Self::get_pending_byte()
2325            .checked_div(page_size)
2326            .map(|val| val + 1)
2327    }
2328
2329    /// Get the maximum page count for this database
2330    pub fn get_max_page_count(&self) -> u32 {
2331        self.max_page_count.load(Ordering::SeqCst)
2332    }
2333
2334    /// Set the maximum page count for this database
2335    /// Returns the new maximum page count (may be clamped to current database size)
2336    pub fn set_max_page_count(&self, new_max: u32) -> crate::Result<IOResult<u32>> {
2337        // Get current database size
2338        let current_page_count =
2339            return_if_io!(self.with_header(|header| header.database_size.get()));
2340
2341        // Clamp new_max to be at least the current database size
2342        let clamped_max = std::cmp::max(new_max, current_page_count);
2343        self.max_page_count.store(clamped_max, Ordering::SeqCst);
2344        Ok(IOResult::Done(clamped_max))
2345    }
2346
2347    pub fn set_wal(&mut self, wal: Arc<dyn Wal>) {
2348        wal.set_io_context(self.io_ctx.read().clone());
2349        self.wal = Some(wal);
2350    }
2351
2352    pub fn get_auto_vacuum_mode(&self) -> AutoVacuumMode {
2353        self.auto_vacuum_mode.load(Ordering::SeqCst).into()
2354    }
2355
2356    pub fn set_auto_vacuum_mode(&self, mode: AutoVacuumMode) {
2357        self.auto_vacuum_mode.store(mode.into(), Ordering::SeqCst);
2358    }
2359
2360    /// Persist the auto-vacuum mode to page 1 and keep the pager cache in sync.
2361    pub fn persist_auto_vacuum_mode(&self, mode: AutoVacuumMode) -> Result<()> {
2362        let (largest_root_page, incremental_vacuum_enabled) = auto_vacuum_header_fields(mode);
2363
2364        if self.db_initialized() {
2365            self.io.block(|| {
2366                self.with_header_mut(|header| {
2367                    header.vacuum_mode_largest_root_page = largest_root_page.into();
2368                    header.incremental_vacuum_enabled = incremental_vacuum_enabled.into();
2369                })
2370            })?;
2371        } else {
2372            let IOResult::Done(_) = self.with_header_mut(|header| {
2373                header.vacuum_mode_largest_root_page = largest_root_page.into();
2374                header.incremental_vacuum_enabled = incremental_vacuum_enabled.into();
2375            })?
2376            else {
2377                panic!("fresh database auto-vacuum setup should not do any IO");
2378            };
2379            // Clear dirty pages since this is pre-initialization setup, not a real write transaction.
2380            // with_header_mut marks page 1 dirty as a side effect, but no transaction is active.
2381            self.dirty_pages.write().clear();
2382        }
2383
2384        self.set_auto_vacuum_mode(mode);
2385        Ok(())
2386    }
2387
2388    /// Retrieves the pointer map entry for a given database page.
2389    /// `target_page_num` (1-indexed) is the page whose entry is sought.
2390    /// Returns `Ok(None)` if the page is not supposed to have a ptrmap entry (e.g. header, or a ptrmap page itself).
2391    #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
2392    pub fn ptrmap_get(&self, target_page_num: u32) -> Result<IOResult<Option<PtrmapEntry>>> {
2393        loop {
2394            let ptrmap_get_state = {
2395                let vacuum_state = self.vacuum_state.read();
2396                vacuum_state.ptrmap_get_state.clone()
2397            };
2398            match ptrmap_get_state {
2399                PtrMapGetState::Start => {
2400                    tracing::trace!("ptrmap_get(page_idx = {})", target_page_num);
2401                    let configured_page_size =
2402                        return_if_io!(self.with_header(|header| header.page_size)).get() as usize;
2403
2404                    if target_page_num < FIRST_PTRMAP_PAGE_NO
2405                        || is_ptrmap_page(target_page_num, configured_page_size)
2406                    {
2407                        return Ok(IOResult::Done(None));
2408                    }
2409
2410                    let ptrmap_pg_no =
2411                        get_ptrmap_page_no_for_db_page(target_page_num, configured_page_size);
2412                    let offset_in_ptrmap_page = get_ptrmap_offset_in_page(
2413                        target_page_num,
2414                        ptrmap_pg_no,
2415                        configured_page_size,
2416                    )?;
2417                    tracing::trace!(
2418                        "ptrmap_get(page_idx = {}) = ptrmap_pg_no = {}",
2419                        target_page_num,
2420                        ptrmap_pg_no
2421                    );
2422
2423                    // `return_if_io!` keeps `ptrmap_get_state` at `Start` on
2424                    // spill so re-entry resumes via pending-read tracking.
2425                    let (ptrmap_page, c) = return_if_io!(self.read_page(ptrmap_pg_no as i64));
2426                    self.vacuum_state.write().ptrmap_get_state = PtrMapGetState::Deserialize {
2427                        ptrmap_page,
2428                        offset_in_ptrmap_page,
2429                    };
2430                    if let Some(c) = c {
2431                        io_yield_one!(c);
2432                    }
2433                }
2434                PtrMapGetState::Deserialize {
2435                    ptrmap_page,
2436                    offset_in_ptrmap_page,
2437                } => {
2438                    turso_assert!(ptrmap_page.is_loaded(), "ptrmap_page should be loaded");
2439                    let page_content = ptrmap_page.get_contents();
2440                    let ptrmap_pg_no = page_content.id;
2441
2442                    let full_buffer_slice: &[u8] = page_content.as_ptr();
2443
2444                    // Ptrmap pages are not page 1, so their internal offset within their buffer should be 0.
2445                    // The actual page data starts at page_content.offset() within the full_buffer_slice.
2446                    if ptrmap_pg_no != 1 && page_content.offset() != 0 {
2447                        return Err(LimboError::Corrupt(format!(
2448                            "Ptrmap page {} has unexpected internal offset {}",
2449                            ptrmap_pg_no,
2450                            page_content.offset()
2451                        )));
2452                    }
2453                    let ptrmap_page_data_slice: &[u8] = &full_buffer_slice[page_content.offset()..];
2454                    let actual_data_length = ptrmap_page_data_slice.len();
2455
2456                    // Check if the calculated offset for the entry is within the bounds of the actual page data length.
2457                    if offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE > actual_data_length {
2458                        return Err(LimboError::InternalError(format!(
2459                        "Ptrmap offset {offset_in_ptrmap_page} + entry size {PTRMAP_ENTRY_SIZE} out of bounds for page {ptrmap_pg_no} (actual data len {actual_data_length})"
2460                    )));
2461                    }
2462
2463                    let entry_slice = &ptrmap_page_data_slice
2464                        [offset_in_ptrmap_page..offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE];
2465                    self.vacuum_state.write().ptrmap_get_state = PtrMapGetState::Start;
2466                    break match PtrmapEntry::deserialize(entry_slice) {
2467                        Some(entry) => Ok(IOResult::Done(Some(entry))),
2468                        None => Err(LimboError::Corrupt(format!(
2469                            "Failed to deserialize ptrmap entry for page {target_page_num} from ptrmap page {ptrmap_pg_no}"
2470                        ))),
2471                    };
2472                }
2473            }
2474        }
2475    }
2476
2477    /// Writes or updates the pointer map entry for a given database page.
2478    /// `db_page_no_to_update` (1-indexed) is the page whose entry is to be set.
2479    /// `entry_type` and `parent_page_no` define the new entry.
2480    #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
2481    pub fn ptrmap_put(
2482        &self,
2483        db_page_no_to_update: u32,
2484        entry_type: PtrmapType,
2485        parent_page_no: u32,
2486    ) -> Result<IOResult<()>> {
2487        tracing::trace!(
2488            "ptrmap_put(page_idx = {}, entry_type = {:?}, parent_page_no = {})",
2489            db_page_no_to_update,
2490            entry_type,
2491            parent_page_no
2492        );
2493        loop {
2494            let ptrmap_put_state = {
2495                let vacuum_state = self.vacuum_state.read();
2496                vacuum_state.ptrmap_put_state.clone()
2497            };
2498            match ptrmap_put_state {
2499                PtrMapPutState::Start => {
2500                    let page_size =
2501                        return_if_io!(self.with_header(|header| header.page_size)).get() as usize;
2502
2503                    if db_page_no_to_update < FIRST_PTRMAP_PAGE_NO
2504                        || is_ptrmap_page(db_page_no_to_update, page_size)
2505                    {
2506                        turso_soft_unreachable!("Cannot set ptrmap entry for header/ptrmap page or invalid page", { "page": db_page_no_to_update });
2507                        return Err(LimboError::InternalError(format!(
2508                        "Cannot set ptrmap entry for page {db_page_no_to_update}: it's a header/ptrmap page or invalid."
2509                    )));
2510                    }
2511
2512                    let ptrmap_pg_no =
2513                        get_ptrmap_page_no_for_db_page(db_page_no_to_update, page_size);
2514                    let offset_in_ptrmap_page =
2515                        get_ptrmap_offset_in_page(db_page_no_to_update, ptrmap_pg_no, page_size)?;
2516                    tracing::trace!(
2517                        "ptrmap_put(page_idx = {}, entry_type = {:?}, parent_page_no = {}) = ptrmap_pg_no = {}, offset_in_ptrmap_page = {}",
2518                        db_page_no_to_update,
2519                        entry_type,
2520                        parent_page_no,
2521                        ptrmap_pg_no,
2522                        offset_in_ptrmap_page
2523                    );
2524
2525                    // `return_if_io!` keeps `ptrmap_put_state` at `Start` on
2526                    // spill so re-entry resumes via pending-read tracking.
2527                    let (ptrmap_page, c) = return_if_io!(self.read_page(ptrmap_pg_no as i64));
2528                    self.vacuum_state.write().ptrmap_put_state = PtrMapPutState::Deserialize {
2529                        ptrmap_page,
2530                        offset_in_ptrmap_page,
2531                    };
2532                    if let Some(c) = c {
2533                        io_yield_one!(c);
2534                    }
2535                }
2536                PtrMapPutState::Deserialize {
2537                    ptrmap_page,
2538                    offset_in_ptrmap_page,
2539                } => {
2540                    turso_assert!(ptrmap_page.is_loaded(), "page should be loaded");
2541                    self.add_dirty(&ptrmap_page)?;
2542                    let page_content = ptrmap_page.get_contents();
2543                    let ptrmap_pg_no = page_content.id;
2544
2545                    let full_buffer_slice = page_content.as_ptr();
2546
2547                    if offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE > full_buffer_slice.len() {
2548                        return Err(LimboError::InternalError(format!(
2549                        "Ptrmap offset {} + entry size {} out of bounds for page {} (actual data len {})",
2550                        offset_in_ptrmap_page,
2551                        PTRMAP_ENTRY_SIZE,
2552                        ptrmap_pg_no,
2553                        full_buffer_slice.len()
2554                    )));
2555                    }
2556
2557                    let entry = PtrmapEntry {
2558                        entry_type,
2559                        parent_page_no,
2560                    };
2561                    entry.serialize(
2562                        &mut full_buffer_slice
2563                            [offset_in_ptrmap_page..offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE],
2564                    )?;
2565
2566                    turso_assert!(
2567                        ptrmap_page.get().id == ptrmap_pg_no,
2568                        "ptrmap page has unexpected number"
2569                    );
2570                    self.vacuum_state.write().ptrmap_put_state = PtrMapPutState::Start;
2571                    break Ok(IOResult::Done(()));
2572                }
2573            }
2574        }
2575    }
2576
2577    /// This method is used to allocate a new root page for a btree, both for tables and indexes
2578    /// FIXME: handle no room in page cache
2579    #[instrument(skip_all, level = Level::DEBUG)]
2580    pub fn btree_create(&self, flags: &CreateBTreeFlags) -> Result<IOResult<u32>> {
2581        let page_type = match flags {
2582            _ if flags.is_table() => PageType::TableLeaf,
2583            _ if flags.is_index() => PageType::IndexLeaf,
2584            _ => unreachable!("Invalid flags state"),
2585        };
2586        #[cfg(clt_turso_feature = "omit_autovacuum")]
2587        {
2588            let page = return_if_io!(self.do_allocate_page(page_type, 0, BtreePageAllocMode::Any));
2589            Ok(IOResult::Done(page.get().id as u32))
2590        }
2591
2592        //  If autovacuum is enabled, we need to allocate a new page number that is greater than the largest root page number
2593        #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
2594        {
2595            let auto_vacuum_mode =
2596                AutoVacuumMode::from(self.auto_vacuum_mode.load(Ordering::SeqCst));
2597            match auto_vacuum_mode {
2598                AutoVacuumMode::None => {
2599                    let page =
2600                        return_if_io!(self.do_allocate_page(page_type, 0, BtreePageAllocMode::Any));
2601                    Ok(IOResult::Done(page.get().id as u32))
2602                }
2603                AutoVacuumMode::Full => {
2604                    loop {
2605                        let btree_create_vacuum_full_state = {
2606                            let vacuum_state = self.vacuum_state.read();
2607                            vacuum_state.btree_create_vacuum_full_state
2608                        };
2609                        match btree_create_vacuum_full_state {
2610                            BtreeCreateVacuumFullState::Start => {
2611                                let (mut root_page_num, page_size) = return_if_io!(self
2612                                    .with_header(|header| {
2613                                        (
2614                                            header.vacuum_mode_largest_root_page.get(),
2615                                            header.page_size.get(),
2616                                        )
2617                                    }));
2618
2619                                turso_assert_greater_than!(root_page_num, 0, "Largest root page number cannot be 0 because that is set to 1 when creating the database with autovacuum enabled");
2620                                root_page_num += 1;
2621                                turso_assert_greater_than_or_equal!(
2622                                    root_page_num,
2623                                    FIRST_PTRMAP_PAGE_NO,
2624                                    "can never be less than 2 because we have already incremented"
2625                                );
2626
2627                                while is_ptrmap_page(root_page_num, page_size as usize) {
2628                                    root_page_num += 1;
2629                                }
2630                                turso_assert_greater_than_or_equal!(
2631                                    root_page_num,
2632                                    3,
2633                                    "root page must be >= 3 (number of the first root page)"
2634                                );
2635                                self.vacuum_state.write().btree_create_vacuum_full_state =
2636                                    BtreeCreateVacuumFullState::AllocatePage { root_page_num };
2637                            }
2638                            BtreeCreateVacuumFullState::AllocatePage { root_page_num } => {
2639                                //  root_page_num here is the desired root page
2640                                let page = return_if_io!(self.do_allocate_page(
2641                                    page_type,
2642                                    0,
2643                                    BtreePageAllocMode::Exact(root_page_num),
2644                                ));
2645                                let allocated_page_id = page.get().id as u32;
2646
2647                                return_if_io!(self.with_header_mut(|header| {
2648                                    if allocated_page_id
2649                                        > header.vacuum_mode_largest_root_page.get()
2650                                    {
2651                                        tracing::debug!(
2652                                            "Updating largest root page in header from {} to {}",
2653                                            header.vacuum_mode_largest_root_page.get(),
2654                                            allocated_page_id
2655                                        );
2656                                        header.vacuum_mode_largest_root_page =
2657                                            allocated_page_id.into();
2658                                    }
2659                                }));
2660
2661                                if allocated_page_id != root_page_num {
2662                                    //  TODO(Zaid): Handle swapping the allocated page with the desired root page
2663                                }
2664
2665                                //  TODO(Zaid): Update the header metadata to reflect the new root page number
2666                                self.vacuum_state.write().btree_create_vacuum_full_state =
2667                                    BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id };
2668                            }
2669                            BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id } => {
2670                                //  For now map allocated_page_id since we are not swapping it with root_page_num
2671                                return_if_io!(self.ptrmap_put(
2672                                    allocated_page_id,
2673                                    PtrmapType::RootPage,
2674                                    0,
2675                                ));
2676                                self.vacuum_state.write().btree_create_vacuum_full_state =
2677                                    BtreeCreateVacuumFullState::Start;
2678                                return Ok(IOResult::Done(allocated_page_id));
2679                            }
2680                        }
2681                    }
2682                }
2683                AutoVacuumMode::Incremental => {
2684                    return Err(LimboError::InternalError(
2685                        "Incremental auto-vacuum is not supported".to_string(),
2686                    ));
2687                }
2688            }
2689        }
2690    }
2691
2692    /// Allocate a new overflow page.
2693    /// This is done when a cell overflows and new space is needed.
2694    // FIXME: handle no room in page cache
2695    pub fn allocate_overflow_page(&self) -> Result<IOResult<PageRef>> {
2696        let page = return_if_io!(self.allocate_page());
2697        tracing::debug!("Pager::allocate_overflow_page(id={})", page.get().id);
2698
2699        // setup overflow page
2700        let contents = page.get_contents();
2701        let buf = contents.as_ptr();
2702        buf.fill(0);
2703
2704        Ok(IOResult::Done(page))
2705    }
2706
2707    /// Allocate a new page to the btree via the pager.
2708    /// This marks the page as dirty and writes the page header.
2709    // FIXME: handle no room in page cache
2710    pub fn do_allocate_page(
2711        &self,
2712        page_type: PageType,
2713        offset: usize,
2714        _alloc_mode: BtreePageAllocMode,
2715    ) -> Result<IOResult<PageRef>> {
2716        let page = return_if_io!(self.allocate_page());
2717        #[cfg(debug_assertions)]
2718        turso_assert_eq!(
2719            offset,
2720            page.get_contents().offset(),
2721            "offset doesn't match computed offset for page"
2722        );
2723        btree_init_page(&page, page_type, offset, self.usable_space());
2724        tracing::debug!(
2725            "do_allocate_page(id={}, page_type={:?})",
2726            page.get().id,
2727            page.get_contents().page_type().ok()
2728        );
2729        Ok(IOResult::Done(page))
2730    }
2731
2732    /// The "usable size" of a database page is the page size specified by the 2-byte integer at offset 16
2733    /// in the header, minus the "reserved" space size recorded in the 1-byte integer at offset 20 in the header.
2734    /// The usable size of a page might be an odd number. However, the usable size is not allowed to be less than 480.
2735    /// In other words, if the page size is 512, then the reserved space size cannot exceed 32.
2736    pub fn usable_space(&self) -> usize {
2737        let page_size = self.get_page_size().unwrap_or_else(|| {
2738            let size = self
2739                .io
2740                .block(|| self.with_header(|header| header.page_size))
2741                .unwrap_or_default();
2742            self.page_size.store(size.get(), Ordering::SeqCst);
2743            size
2744        });
2745
2746        let reserved_space = self.get_reserved_space().unwrap_or_else(|| {
2747            let space = if self.db_initialized() {
2748                self.io
2749                    .block(|| self.with_header(|header| header.reserved_space))
2750                    .unwrap_or_default()
2751            } else {
2752                // Before page 1 is allocated, the in-memory bootstrap header may still carry
2753                // reserved_space=0. Use IOContext so checksum/encryption-required tail bytes are
2754                // respected when computing usable space for first writes.
2755                self.io_ctx.read().get_reserved_space_bytes()
2756            };
2757            self.set_reserved_space(space);
2758            space
2759        });
2760
2761        (page_size.get() as usize) - (reserved_space as usize)
2762    }
2763
2764    pub fn db_initialized(&self) -> bool {
2765        self.init_page_1.load().is_none()
2766    }
2767
2768    /// Set the initial page size for the database. Should only be called before the database is initialized
2769    pub fn set_initial_page_size(&self, size: PageSize) -> Result<()> {
2770        turso_assert!(!self.db_initialized());
2771        let IOResult::Done(mut header) = self.with_header(|header| *header)? else {
2772            panic!("DB should not be initialized and should not do any IO");
2773        };
2774        header.page_size = size;
2775
2776        let page = Arc::new(Page::new(DatabaseHeader::PAGE_ID as i64));
2777        {
2778            let inner = page.get();
2779            inner.buffer = Some(Arc::new(Buffer::new_temporary(size.get() as usize)));
2780        }
2781
2782        page.get_contents().write_database_header(&header);
2783        page.set_loaded();
2784        page.clear_wal_tag();
2785
2786        btree_init_page(
2787            &page,
2788            PageType::TableLeaf,
2789            DatabaseHeader::SIZE,
2790            (size.get() - header.reserved_space as u32) as usize,
2791        );
2792
2793        self.init_page_1.store(Some(page));
2794        self.page_size.store(size.get(), Ordering::SeqCst);
2795        // Clear dirty pages since this is pre-initialization setup, not a real write transaction.
2796        // Rebuilding init_page_1 must not leak any stale 4 KiB page-1 image into the first write.
2797        self.dirty_pages.write().clear();
2798        Ok(())
2799    }
2800
2801    /// Set the initial journal version in page 1 before the database is initialized.
2802    pub fn set_initial_journal_version(&self, version: sqlite3_ondisk::Version) -> Result<()> {
2803        turso_assert!(!self.db_initialized());
2804        let raw_version = sqlite3_ondisk::RawVersion::from(version);
2805        let IOResult::Done(_) = self.with_header_mut(|header| {
2806            header.read_version = raw_version;
2807            header.write_version = raw_version;
2808        })?
2809        else {
2810            panic!("DB should not be initialized and should not do any IO");
2811        };
2812        // Clear dirty pages since this is pre-initialization setup, not a real write transaction.
2813        // with_header_mut marks page 1 dirty as a side effect, but no transaction is active.
2814        self.dirty_pages.write().clear();
2815        Ok(())
2816    }
2817
2818    /// Get the current page size. Returns None if not set yet.
2819    pub fn get_page_size(&self) -> Option<PageSize> {
2820        let value = self.page_size.load(Ordering::SeqCst);
2821        if value == 0 {
2822            None
2823        } else {
2824            PageSize::new(value)
2825        }
2826    }
2827
2828    /// Get the current page size, panicking if not set.
2829    pub fn get_page_size_unchecked(&self) -> PageSize {
2830        let value = self.page_size.load(Ordering::SeqCst);
2831        turso_assert_ne!(value, 0);
2832        PageSize::new(value).expect("invalid page size stored")
2833    }
2834
2835    pub(crate) fn has_wal(&self) -> bool {
2836        self.wal.is_some()
2837    }
2838
2839    #[cfg(clt_turso_tests)]
2840    pub(crate) fn wal_shared_ptr(&self) -> Option<usize> {
2841        self.wal
2842            .as_ref()
2843            .and_then(|wal| wal.as_any().downcast_ref::<crate::storage::wal::WalFile>())
2844            .map(crate::storage::wal::WalFile::shared_ptr)
2845    }
2846
2847    /// Set the page size. Used internally when page size is determined.
2848    pub fn set_page_size(&self, size: PageSize) {
2849        self.page_size.store(size.get(), Ordering::SeqCst);
2850    }
2851
2852    /// Get the current reserved space. Returns None if not set yet.
2853    pub fn get_reserved_space(&self) -> Option<u8> {
2854        let value = self.reserved_space.load(Ordering::SeqCst);
2855        if value == RESERVED_SPACE_NOT_SET {
2856            None
2857        } else {
2858            Some(value as u8)
2859        }
2860    }
2861
2862    /// Set the reserved space. Must fit in u8.
2863    pub fn set_reserved_space(&self, space: u8) {
2864        self.reserved_space.store(space as u16, Ordering::SeqCst);
2865    }
2866
2867    /// Schema cookie sentinel value that represents value not set.
2868    const SCHEMA_COOKIE_NOT_SET: u64 = u64::MAX;
2869
2870    /// Get the cached schema cookie. Returns None if not set yet.
2871    pub fn get_schema_cookie_cached(&self) -> Option<u32> {
2872        let value = self.schema_cookie.load(Ordering::SeqCst);
2873        if value == Self::SCHEMA_COOKIE_NOT_SET {
2874            None
2875        } else {
2876            Some(value as u32)
2877        }
2878    }
2879
2880    /// Set the schema cookie cache.
2881    pub fn set_schema_cookie(&self, cookie: Option<u32>) {
2882        let value = cookie.map_or(Self::SCHEMA_COOKIE_NOT_SET, |v| v as u64);
2883        self.schema_cookie.store(value, Ordering::SeqCst);
2884    }
2885
2886    /// Get the schema cookie, using the cached value if available to avoid reading page 1.
2887    pub fn get_schema_cookie(&self) -> Result<IOResult<u32>> {
2888        // Try to use cached value first
2889        if let Some(cookie) = self.get_schema_cookie_cached() {
2890            return Ok(IOResult::Done(cookie));
2891        }
2892        // If not cached, read from header and cache it
2893        self.with_header(|header| header.schema_cookie.get())
2894    }
2895
2896    /// This connection's frozen WAL position `(checkpoint_seq, max_frame)` — the read mark for a
2897    /// reader, or the post-commit position for a writer. `(u32::MAX, u64::MAX)` when there is no
2898    /// WAL (no WAL materialization hazard). See `Wal::connection_wal_pos`.
2899    pub fn wal_pos(&self) -> (u32, u64) {
2900        self.wal
2901            .as_ref()
2902            .map_or((u32::MAX, u64::MAX), |wal| wal.connection_wal_pos())
2903    }
2904
2905    /// Lowest WAL frame any active reader is pinned at, or `None` if none / no WAL. Used as the
2906    /// Passive-checkpoint version-store GC floor (includes readers pinned via `begin_read_tx`
2907    /// before they publish an MVCC transaction). See `MvStore::rootpage_gc_protected`.
2908    pub fn min_pinned_read_frame(&self) -> Option<u64> {
2909        self.wal
2910            .as_ref()
2911            .and_then(|wal| wal.min_pinned_read_frame())
2912    }
2913
2914    /// The WAL backfill boundary (frames at or below this are durable in the DB file). The MVCC
2915    /// Version-store GC floor for passive checkpoints: a materialized version may be reclaimed only
2916    /// once its materialization frame is backfilled here, so every snapshot can read it from the btree.
2917    pub fn wal_backfill_frame(&self) -> Option<u64> {
2918        self.wal.as_ref().map(|wal| wal.backfill_frame())
2919    }
2920
2921    #[inline(always)]
2922    #[instrument(skip_all, level = Level::DEBUG)]
2923    pub fn begin_read_tx(&self) -> Result<()> {
2924        let Some(wal) = self.wal.as_ref() else {
2925            return Ok(());
2926        };
2927        let changed = wal.begin_read_tx()?;
2928        if changed {
2929            // Someone else changed the database -> assume our page cache is invalid (this is default SQLite behavior, we can probably do better with more granular invalidation)
2930            self.clear_page_cache(false);
2931            // Invalidate cached schema cookie to force re-read on next access
2932            self.set_schema_cookie(None);
2933        }
2934        Ok(())
2935    }
2936
2937    /// MVCC-only: refresh connection-private WAL change counters without starting a read tx and invalidate cache if needed.
2938    pub fn mvcc_refresh_if_db_changed(&self) {
2939        let Some(wal) = self.wal.as_ref() else {
2940            return;
2941        };
2942        if wal.mvcc_refresh_if_db_changed() {
2943            // Prevents stale page cache reads after MVCC checkpoints update the DB file.
2944            self.clear_page_cache(false);
2945            self.set_schema_cookie(None);
2946        }
2947    }
2948
2949    #[instrument(skip_all, level = Level::DEBUG)]
2950    pub fn maybe_allocate_page1(&self) -> Result<IOResult<()>> {
2951        if !self.db_initialized() {
2952            if let Some(_lock) = self.init_lock.try_lock() {
2953                return Ok(self.allocate_page1()?.map(|_| ()));
2954            }
2955            // Give a chance for the allocation to happen elsewhere
2956            io_yield_one!(Completion::new_yield());
2957        }
2958        Ok(IOResult::Done(()))
2959    }
2960
2961    #[inline(always)]
2962    #[instrument(skip_all, level = Level::DEBUG)]
2963    /// `allowed_auto_actions` controls which automatic WAL maintenance the
2964    /// caller permits during this begin. The only action consulted here is
2965    /// `WalAutoActions::Restart`, which gates the WAL-header restart inside
2966    /// `try_restart_log_before_write`. Callers managing WAL state externally
2967    /// (sync engine) must not pass `Restart` because rotating the WAL header
2968    /// behind their back invalidates watermarks they have already published.
2969    pub fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<IOResult<()>> {
2970        // TODO(Diego): The only possibly allocate page1 here is because OpenEphemeral needs a write transaction
2971        // we should have a unique API to begin transactions, something like sqlite3BtreeBeginTrans
2972        return_if_io!(self.maybe_allocate_page1());
2973        let Some(wal) = self.wal.as_ref() else {
2974            return Ok(IOResult::Done(()));
2975        };
2976        wal.begin_write_tx(allowed_auto_actions)?;
2977        // Must run after the upgrade (and any log restart it performed) so
2978        // the positions belong to the current WAL generation.
2979        self.materialize_savepoint_wal_positions();
2980        Ok(IOResult::Done(()))
2981    }
2982
2983    /// Fill in the WAL position of savepoints opened before this write
2984    /// transaction, mirroring SQLite's `sqlite3PagerOpenSavepoint` at
2985    /// write-transaction begin. Idempotent: only fills unmaterialized
2986    /// positions, so upgrade retry loops (Busy/BusySnapshot) are safe.
2987    fn materialize_savepoint_wal_positions(&self) {
2988        let Some(wal) = self.wal.as_ref() else {
2989            return;
2990        };
2991        let pos = SavepointWalPos {
2992            max_frame: wal.get_max_frame(),
2993            checksum: wal.get_last_checksum(),
2994            checkpoint_seq: wal.get_checkpoint_seq(),
2995        };
2996        for savepoint in self.savepoints.read().iter() {
2997            let mut wal_pos = savepoint.wal_pos.write();
2998            if wal_pos.is_none() {
2999                *wal_pos = Some(pos);
3000            }
3001        }
3002    }
3003
3004    /// Acquire exclusive WAL access + block new transactions (used by VACUUM).
3005    ///
3006    /// This is a blocking alternative to normal `begin_read_tx`.
3007    ///
3008    /// VACUUM runs on an existing database, so page 1 must already be allocated
3009    /// and a WAL must be present.
3010    pub fn begin_vacuum_blocking_tx(&self) -> Result<IOResult<()>> {
3011        if !self.db_initialized() {
3012            return Err(LimboError::InternalError(
3013                "begin_vacuum_blocking_tx can be done on an initialized database (page 1 must already be allocated)".into(),
3014            ));
3015        }
3016        let wal = self.wal.as_ref().ok_or_else(|| {
3017            LimboError::InternalError("begin_vacuum_blocking_tx requires WAL mode".into())
3018        })?;
3019        wal.begin_vacuum_blocking_tx()?;
3020        // let's be conservative and clear all cache for vacuum
3021        // todo: clear cache only if we detect that new writes have occurred like `begin_read_tx`
3022        self.clear_page_cache(false);
3023        self.set_schema_cookie(None);
3024        Ok(IOResult::Done(()))
3025    }
3026
3027    /// commit dirty pages from current transaction in WAL mode if this is not nested statement (for nested statements, parent will do the commit)
3028    /// if update_transaction_state set to false, then [Connection::transaction_state] left unchanged
3029    /// if update_transaction_state set to true, then [Connection::transaction_state] reset to [TransactionState::None] in case when method completes without error
3030    #[instrument(skip_all, level = Level::DEBUG)]
3031    pub fn commit_tx(
3032        &self,
3033        connection: &Connection,
3034        update_transaction_state: bool,
3035    ) -> Result<IOResult<()>> {
3036        if connection.is_nested_stmt() {
3037            // Parent statement will handle the transaction commit.
3038            return Ok(IOResult::Done(()));
3039        }
3040        let Some(wal) = self.wal.as_ref() else {
3041            // TODO: Unsure what the semantics of "end_tx" is for in-memory databases, ephemeral tables and ephemeral indexes.
3042            self.clear_savepoints()?;
3043            return Ok(IOResult::Done(()));
3044        };
3045
3046        let complete_commit = || {
3047            if update_transaction_state {
3048                connection.set_tx_state(TransactionState::None);
3049            }
3050            self.commit_wal_end();
3051        };
3052
3053        loop {
3054            let commit_state = self.commit_info.read().state;
3055            tracing::debug!("commit_state: {:?}", commit_state);
3056            // we separate auto-checkpoint from the commit in order for checkpoint to be able to backfill WAL till the end
3057            // (including new frames from current transaction)
3058            // otherwise, we will be unable to do WAL restart
3059            match commit_state {
3060                CommitState::AutoCheckpoint => {
3061                    let checkpoint_result = self.checkpoint(
3062                        CheckpointMode::Passive {
3063                            upper_bound_inclusive: None,
3064                        },
3065                        connection.get_sync_mode(),
3066                        false,
3067                    );
3068                    match checkpoint_result {
3069                        Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
3070                        Ok(IOResult::Done(_)) => complete_commit(),
3071                        Err(err) => {
3072                            tracing::debug!("auto-checkpoint failed: {err}");
3073                            complete_commit();
3074                            self.cleanup_after_auto_checkpoint_failure();
3075                        }
3076                    }
3077                    self.clear_savepoints()?;
3078                    return Ok(IOResult::Done(()));
3079                }
3080                _ => {
3081                    return_if_io!(self.commit_wal(
3082                        connection.wal_auto_actions(),
3083                        connection.get_sync_mode(),
3084                        connection.get_data_sync_retry(),
3085                    ));
3086
3087                    let schema_did_change = match connection.get_tx_state() {
3088                        TransactionState::Write { schema_did_change } => schema_did_change,
3089                        _ => false,
3090                    };
3091
3092                    wal.end_write_tx();
3093                    wal.end_read_tx();
3094                    // we do not set TransactionState::None here - because caller can decide that nothing should be done for this connection
3095                    // and skip next calls of the commit_tx methods after IO
3096
3097                    tracing::debug!("commit_tx: schema_did_change={schema_did_change}");
3098                    if schema_did_change {
3099                        let schema = connection.schema.read().clone();
3100                        connection.db.update_schema_if_newer(schema);
3101                    }
3102
3103                    if self.commit_info.read().state != CommitState::AutoCheckpoint {
3104                        complete_commit();
3105                        self.clear_savepoints()?;
3106                        return Ok(IOResult::Done(()));
3107                    }
3108                }
3109            }
3110        }
3111    }
3112
3113    #[instrument(skip_all, level = Level::DEBUG)]
3114    pub fn rollback_tx(&self, connection: &Connection) {
3115        if connection.is_nested_stmt() {
3116            // Parent statement will handle the transaction rollback.
3117            return;
3118        }
3119        let Some(wal) = self.wal.as_ref() else {
3120            // TODO: Unsure what the semantics of "end_tx" is for in-memory databases, ephemeral tables and ephemeral indexes.
3121            return;
3122        };
3123        let (is_write, schema_did_change) = match connection.get_tx_state() {
3124            TransactionState::Write { schema_did_change } => (true, schema_did_change),
3125            _ => (false, false),
3126        };
3127        tracing::trace!("rollback_tx(schema_did_change={})", schema_did_change);
3128        if is_write {
3129            self.clear_savepoints()
3130                .expect("in practice, clear_savepoints() should never fail as it uses memory IO");
3131            // IMPORTANT: rollback() must be called BEFORE end_write_tx() releases the write_lock.
3132            // Otherwise, another thread could commit new frames to frame_cache between
3133            // end_write_tx() and rollback(), and rollback() would incorrectly remove them.
3134            self.rollback(schema_did_change, connection, is_write);
3135            wal.end_write_tx();
3136        } else {
3137            self.rollback(schema_did_change, connection, is_write);
3138        }
3139        wal.end_read_tx();
3140    }
3141
3142    pub(crate) fn cleanup_read_tx(&self) {
3143        let Some(wal) = self.wal.as_ref() else {
3144            return;
3145        };
3146        self.reset_internal_states();
3147        if wal.holds_read_lock() {
3148            wal.end_read_tx();
3149        }
3150    }
3151
3152    #[instrument(skip_all, level = Level::DEBUG)]
3153    pub fn end_read_tx(&self) {
3154        let Some(wal) = self.wal.as_ref() else {
3155            return;
3156        };
3157        wal.end_read_tx();
3158    }
3159
3160    /// End just the write transaction on the WAL, without affecting the read lock.
3161    pub fn end_write_tx(&self) {
3162        let Some(wal) = self.wal.as_ref() else {
3163            return;
3164        };
3165        wal.end_write_tx();
3166    }
3167
3168    /// Returns true if this pager's WAL currently holds a read lock.
3169    pub fn holds_read_lock(&self) -> bool {
3170        let Some(wal) = self.wal.as_ref() else {
3171            return false;
3172        };
3173        wal.holds_read_lock()
3174    }
3175
3176    pub fn holds_write_lock(&self) -> bool {
3177        let Some(wal) = self.wal.as_ref() else {
3178            return false;
3179        };
3180        wal.holds_write_lock()
3181    }
3182
3183    /// Rollback and clean up an attached database pager's transaction.
3184    /// Unlike rollback_tx, this doesn't modify connection-level state.
3185    pub fn rollback_attached(&self) {
3186        let Some(wal) = self.wal.as_ref() else {
3187            return;
3188        };
3189        let is_write = wal.holds_write_lock();
3190        if is_write {
3191            self.clear_savepoints()
3192                .expect("clear_savepoints should not fail for attached DB");
3193            // Clear dirty pages and page cache before releasing the write lock
3194            self.clear_page_cache(true);
3195            self.dirty_pages.write().clear();
3196            self.reset_internal_states();
3197            self.set_schema_cookie(None);
3198            wal.rollback(None);
3199            wal.end_write_tx();
3200        } else {
3201            self.cleanup_read_tx();
3202        }
3203        if wal.holds_read_lock() {
3204            wal.end_read_tx();
3205        }
3206    }
3207
3208    /// Reads a page from disk (either WAL or DB file) bypassing page-cache
3209    #[tracing::instrument(skip_all, level = Level::DEBUG)]
3210    pub fn read_page_no_cache(
3211        &self,
3212        page_idx: i64,
3213        frame_watermark: Option<u64>,
3214        allow_empty_read: bool,
3215    ) -> Result<(PageRef, Completion)> {
3216        turso_assert_greater_than_or_equal!(page_idx, 0);
3217        tracing::debug!("read_page_no_cache(page_idx = {})", page_idx);
3218        let page = Arc::new(Page::new(page_idx));
3219        let io_ctx = self.io_ctx.read();
3220        let Some(wal) = self.wal.as_ref() else {
3221            turso_assert!(
3222                matches!(frame_watermark, Some(0) | None),
3223                "frame_watermark must be either None or Some(0) because DB has no WAL and read with other watermark is invalid"
3224            );
3225
3226            page.set_locked();
3227            let c = self.begin_read_disk_page(
3228                page_idx as usize,
3229                page.clone(),
3230                allow_empty_read,
3231                &io_ctx,
3232            )?;
3233            return Ok((page, c));
3234        };
3235
3236        if let Some(frame_id) = wal.find_frame(page_idx as u64, frame_watermark)? {
3237            let c = wal.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
3238            // TODO(pere) should probably first insert to page cache, and if successful,
3239            // read frame or page
3240            return Ok((page, c));
3241        }
3242
3243        page.set_locked();
3244        let c =
3245            self.begin_read_disk_page(page_idx as usize, page.clone(), allow_empty_read, &io_ctx)?;
3246        Ok((page, c))
3247    }
3248
3249    /// Issue a non-blocking page read, inserting into the page cache, may spill to disk.
3250    ///
3251    /// * `Done((page, None))`: page was already in cache, no IO needed.
3252    /// * `Done((page, Some(c_disk)))`: page was not in cache; it has been
3253    ///   inserted into the cache and a disk-read is in flight against it.
3254    ///   The caller must yield on `c_disk` before reading `page` contents.
3255    /// * `IO(c_spill)`: the page cache was full and a spill is in flight.
3256    ///   Caller must yield on `c_spill` and then call `read_page_nonblock(idx)`
3257    ///   again. The disk read for this page has already been issued and will
3258    ///   be reused on re-entry via `pending_reads` (no duplicate IO).
3259    ///
3260    /// Re-entrancy contract: the caller may invoke this with the same
3261    /// `page_idx` arbitrarily many times. Each `Some(page_idx)` mapping in
3262    /// `pending_reads` corresponds to a single outstanding disk read; the
3263    /// entry is removed exactly when this method returns `Done`.
3264    #[tracing::instrument(skip_all, level = Level::TRACE)]
3265    pub fn read_page(&self, page_idx: i64) -> Result<IOResult<(PageRef, Option<Completion>)>> {
3266        turso_assert_greater_than_or_equal!(page_idx, 0, "pages in pager should be positive, negative might indicate unallocated pages from mvcc or any other nasty bug");
3267        tracing::debug!("read_page_nonblock(page_idx = {})", page_idx);
3268        #[cfg(clt_turso_tests)]
3269        if self.spill_yield.should_yield_for(page_idx) {
3270            io_yield_one!(crate::Completion::new_yield());
3271        }
3272        let pending = self.pending_reads.read().get(&page_idx).cloned();
3273        let (page, c_disk) = if let Some(pending) = pending {
3274            // Re-entry: previous call yielded on spill before completing
3275            // `cache_insert`. Reuse the same PageRef and in-flight disk read
3276            // rather than issuing duplicate IO.
3277            (pending.page, pending.disk_read)
3278        } else {
3279            // Fast path: cache hit.
3280            {
3281                let mut page_cache = self.page_cache.write();
3282                let page_key = PageCacheKey::new(page_idx as usize);
3283                if let Some(page) = page_cache.get(&page_key)? {
3284                    turso_assert!(
3285                        page_idx as usize == page.get().id,
3286                        "attempted to read page but got different page",
3287                        { "expected_page": page_idx, "actual_page": page.get().id }
3288                    );
3289                    if !page.is_loaded() {
3290                        // The page is cache-resident but its read is still in
3291                        // flight: `read_page` publishes a page into the shared
3292                        // cache (via `cache_insert` below) *before* its disk
3293                        // read completes, and `PageCache::get` deliberately
3294                        // hands out locked-but-unloaded in-flight pages. We have
3295                        // no completion to surface on this path (the disk-read
3296                        // completion was consumed by the original caller and the
3297                        // `pending_reads` entry has already been removed), so
3298                        // returning `Done((page, None))` would hand the caller a
3299                        // locked, unloaded page with nothing to wait on: a torn
3300                        // / uninitialized read, or a concurrent writer filling
3301                        // the buffer underneath the reader.
3302                        io_yield_one!(crate::Completion::new_yield());
3303                    }
3304                    return Ok(IOResult::Done((page, None)));
3305                }
3306            }
3307
3308            tracing::debug!("read_page(page_idx = {page_idx}) = reading page from disk");
3309            let (page, c) = self.read_page_no_cache(page_idx, None, false)?;
3310            self.pending_reads.write().insert(
3311                page_idx,
3312                PendingRead {
3313                    page: page.clone(),
3314                    disk_read: Some(c.clone()),
3315                },
3316            );
3317            (page, Some(c))
3318        };
3319
3320        match self.cache_insert(page_idx as usize, page.clone())? {
3321            IOResult::Done(()) => {
3322                self.pending_reads.write().remove(&page_idx);
3323                Ok(IOResult::Done((page, c_disk)))
3324            }
3325            IOResult::IO(IOCompletions::Single(spill_c)) => {
3326                // Leave the pending entry in place; the next call to
3327                // `read_page_nonblock(page_idx)` will recover it and retry
3328                // `cache_insert` without re-issuing the disk read.
3329                io_yield_one!(spill_c);
3330            }
3331        }
3332    }
3333
3334    fn begin_read_disk_page(
3335        &self,
3336        page_idx: usize,
3337        page: PageRef,
3338        allow_empty_read: bool,
3339        io_ctx: &IOContext,
3340    ) -> Result<Completion> {
3341        sqlite3_ondisk::begin_read_page(
3342            self.db_file.as_ref(),
3343            self.buffer_pool.clone(),
3344            page,
3345            page_idx,
3346            allow_empty_read,
3347            io_ctx,
3348        )
3349    }
3350
3351    /// Insert a page into the cache, with spilling support.
3352    /// This handles cache full conditions by spilling dirty pages and retrying.
3353    /// The cache capacity is a soft limit: if nothing can be spilled or
3354    /// evicted, the page is admitted over capacity rather than failing the
3355    /// read (mirroring SQLite, where `cache_size` may be exceeded while all
3356    /// pages are in use); later inserts drain the excess.
3357    fn cache_insert(&self, page_idx: usize, page: PageRef) -> Result<IOResult<()>> {
3358        {
3359            let mut page_cache = self.page_cache.write();
3360            let page_key = PageCacheKey::new(page_idx);
3361            match page_cache.insert(page_key, page.clone()) {
3362                Ok(_) => return Ok(IOResult::Done(())),
3363                Err(CacheError::KeyExists) => {
3364                    unreachable!("Page should not exist in cache after get() miss");
3365                }
3366                Err(CacheError::Full) => {
3367                    // Fall through to spilling
3368                }
3369                Err(e) => return Err(e.into()),
3370            }
3371        }
3372
3373        match self.try_spill_dirty_pages()? {
3374            IOResult::Done(()) => {
3375                let mut page_cache = self.page_cache.write();
3376                let page_key = PageCacheKey::new(page_idx);
3377                match page_cache.force_insert_page(page_key, page) {
3378                    Ok(_) => Ok(IOResult::Done(())),
3379                    Err(CacheError::KeyExists) => Ok(IOResult::Done(())),
3380                    Err(e) => Err(e.into()),
3381                }
3382            }
3383            IOResult::IO(c) => Ok(IOResult::IO(c)),
3384        }
3385    }
3386
3387    /// Test-only: arm `read_page` to return `IO(yield)` once for `page_id`
3388    /// after `skip` matching calls have passed through.
3389    #[cfg(clt_turso_tests)]
3390    pub(crate) fn arm_spill_yield_on_read(&self, page_id: i64, skip: usize) {
3391        self.spill_yield.arm(page_id, skip);
3392    }
3393
3394    // Get a page from the cache, if it exists.
3395    pub fn cache_get(&self, page_idx: usize) -> Result<Option<PageRef>> {
3396        tracing::trace!("read_page(page_idx = {})", page_idx);
3397        let mut page_cache = self.page_cache.write();
3398        let page_key = PageCacheKey::new(page_idx);
3399        page_cache.get(&page_key)
3400    }
3401
3402    /// Get a page from cache only if it matches the target frame
3403    pub fn cache_get_for_checkpoint(
3404        &self,
3405        page_idx: usize,
3406        target_frame: u64,
3407        seq: u32,
3408    ) -> Result<Option<PageRef>> {
3409        let mut page_cache = self.page_cache.write();
3410        let page_key = PageCacheKey::new(page_idx);
3411        let page = page_cache.get(&page_key)?.and_then(|page| {
3412            if page.is_valid_for_checkpoint(target_frame, seq) {
3413                tracing::debug!(
3414                    "cache_get_for_checkpoint: page {page_idx} frame {target_frame} is valid",
3415                );
3416                Some(page)
3417            } else {
3418                tracing::trace!(
3419                    "cache_get_for_checkpoint: page {} has frame/tag {:?}: (dirty={}), need frame {} and seq {seq}",
3420                    page_idx,
3421                    page.wal_tag_pair(),
3422                    page.is_dirty(),
3423                    target_frame
3424                );
3425                None
3426            }
3427        });
3428        Ok(page)
3429    }
3430
3431    /// Changes the size of the page cache.
3432    pub fn change_page_cache_size(&self, capacity: usize) -> Result<CacheResizeResult> {
3433        let mut page_cache = self.page_cache.write();
3434        Ok(page_cache.resize(capacity))
3435    }
3436
3437    pub fn add_dirty(&self, page: &Page) -> Result<()> {
3438        turso_assert!(
3439            page.is_loaded(),
3440            "page must be loaded in add_dirty() so its contents can be subjournaled",
3441            { "page_id": page.get().id }
3442        );
3443        self.subjournal_page_if_required(page)?;
3444        let mut dirty_pages = self.dirty_pages.write();
3445        dirty_pages.insert(page.get().id as u32);
3446        // Notify cache before marking dirty (page was evictable, now it won't be)
3447        // Only notify if page wasn't already dirty, or if it was spilled
3448        // State before set_dirty():
3449        // - clean page: evictable -> set_dirty() makes it dirty and unevictable
3450        // - dirty + spilled page: evictable -> set_dirty() clears spilled and makes it unevictable
3451        // - dirty + not spilled page: already unevictable -> no cache accounting change
3452        if !page.is_dirty() || page.is_spilled() {
3453            let key = PageCacheKey::new(page.get().id);
3454            self.page_cache.write().notify_page_dirty(key);
3455        }
3456        page.set_dirty();
3457        Ok(())
3458    }
3459
3460    pub fn wal_state(&self) -> Result<WalState> {
3461        let Some(wal) = self.wal.as_ref() else {
3462            turso_soft_unreachable!("wal_state() called on database without WAL");
3463            return Err(LimboError::InternalError(
3464                "wal_state() called on database without WAL".to_string(),
3465            ));
3466        };
3467        Ok(WalState {
3468            checkpoint_seq_no: wal.get_checkpoint_seq(),
3469            max_frame: wal.get_max_frame(),
3470        })
3471    }
3472
3473    /// Flush all dirty pages to disk (async/re-entrant).
3474    /// Unlike commit_wal, this function does not commit, checkpoint nor sync the WAL/Database.
3475    #[instrument(skip_all, level = Level::DEBUG)]
3476    pub fn cacheflush(&self) -> Result<IOResult<Vec<Completion>>> {
3477        let wal = self
3478            .wal
3479            .as_ref()
3480            .ok_or_else(|| LimboError::InternalError("cacheflush() called without WAL".into()))?;
3481        let page_sz = self.get_page_size().unwrap_or_default();
3482
3483        loop {
3484            let phase = std::mem::take(&mut *self.cacheflush_state.write());
3485
3486            match self.cacheflush_step(wal, page_sz, phase)? {
3487                CacheFlushStep::Yield(next_phase, io) => {
3488                    *self.cacheflush_state.write() = next_phase;
3489                    return Ok(IOResult::IO(io));
3490                }
3491                CacheFlushStep::Continue(next_phase) => {
3492                    *self.cacheflush_state.write() = next_phase;
3493                }
3494                CacheFlushStep::Done(completions) => {
3495                    *self.cacheflush_state.write() = CacheFlushState::Init;
3496                    return Ok(IOResult::Done(completions));
3497                }
3498            }
3499        }
3500    }
3501
3502    /// Executes one step of the cache flush state machine.
3503    #[inline]
3504    fn cacheflush_step(
3505        &self,
3506        wal: &Arc<dyn Wal>,
3507        page_sz: PageSize,
3508        phase: CacheFlushState,
3509    ) -> Result<CacheFlushStep> {
3510        match phase {
3511            CacheFlushState::Init => self.cacheflush_init(wal, page_sz),
3512            CacheFlushState::WalPrepareStart {
3513                dirty_ids,
3514                completion,
3515            } => self.cacheflush_wal_prepare_start(wal, dirty_ids, completion),
3516            CacheFlushState::WalPrepareFinish {
3517                dirty_ids,
3518                completion,
3519            } => self.cacheflush_wal_prepare_finish(dirty_ids, completion),
3520            CacheFlushState::Collecting(state) => self.cacheflush_collect(wal, page_sz, state),
3521            CacheFlushState::WaitingForRead {
3522                state,
3523                page_id,
3524                page,
3525                completion,
3526            } => self.cacheflush_handle_read(wal, page_sz, state, page_id, page, completion),
3527        }
3528    }
3529
3530    /// Init phase: gather dirty page IDs and begin WAL preparation.
3531    fn cacheflush_init(&self, wal: &Arc<dyn Wal>, page_sz: PageSize) -> Result<CacheFlushStep> {
3532        let dirty_ids: Vec<usize> = self.dirty_pages.read().iter().map(|x| x as usize).collect();
3533
3534        if dirty_ids.is_empty() {
3535            return Ok(CacheFlushStep::Done(Vec::new()));
3536        }
3537
3538        // Start WAL preparation
3539        match wal.prepare_wal_start(page_sz)? {
3540            Some(completion) => Ok(CacheFlushStep::Yield(
3541                CacheFlushState::WalPrepareStart {
3542                    dirty_ids,
3543                    completion: completion.clone(),
3544                },
3545                IOCompletions::Single(completion),
3546            )),
3547            None => {
3548                // No async prep needed, go straight to finish
3549                let completion = wal.prepare_wal_finish(self.get_sync_type())?;
3550                Ok(CacheFlushStep::Yield(
3551                    CacheFlushState::WalPrepareFinish {
3552                        dirty_ids,
3553                        completion: completion.clone(),
3554                    },
3555                    IOCompletions::Single(completion),
3556                ))
3557            }
3558        }
3559    }
3560
3561    #[inline]
3562    /// Wait for WAL prepare_start, then call prepare_finish.
3563    fn cacheflush_wal_prepare_start(
3564        &self,
3565        wal: &Arc<dyn Wal>,
3566        dirty_ids: Vec<usize>,
3567        completion: Completion,
3568    ) -> Result<CacheFlushStep> {
3569        if !completion.succeeded() {
3570            return Ok(CacheFlushStep::Yield(
3571                CacheFlushState::WalPrepareStart {
3572                    dirty_ids,
3573                    completion: completion.clone(),
3574                },
3575                IOCompletions::Single(completion),
3576            ));
3577        }
3578
3579        let finish_completion = wal.prepare_wal_finish(self.get_sync_type())?;
3580        Ok(CacheFlushStep::Yield(
3581            CacheFlushState::WalPrepareFinish {
3582                dirty_ids,
3583                completion: finish_completion.clone(),
3584            },
3585            IOCompletions::Single(finish_completion),
3586        ))
3587    }
3588
3589    #[inline]
3590    /// Wait for WAL prepare_finish, then start collecting pages.
3591    fn cacheflush_wal_prepare_finish(
3592        &self,
3593        dirty_ids: Vec<usize>,
3594        completion: Completion,
3595    ) -> Result<CacheFlushStep> {
3596        if !completion.succeeded() {
3597            return Ok(CacheFlushStep::Yield(
3598                CacheFlushState::WalPrepareFinish {
3599                    dirty_ids,
3600                    completion: completion.clone(),
3601                },
3602                IOCompletions::Single(completion),
3603            ));
3604        }
3605
3606        Ok(CacheFlushStep::Continue(CacheFlushState::Collecting(
3607            CollectingState {
3608                dirty_ids,
3609                current_idx: 0,
3610                collected_pages: Vec::new(),
3611                completions: Vec::new(),
3612            },
3613        )))
3614    }
3615
3616    #[inline]
3617    /// Main collection loop: fetch pages from cache, handle evictions, write batches.
3618    fn cacheflush_collect(
3619        &self,
3620        wal: &Arc<dyn Wal>,
3621        page_sz: PageSize,
3622        mut state: CollectingState,
3623    ) -> Result<CacheFlushStep> {
3624        while state.current_idx < state.dirty_ids.len() {
3625            let page_id = state.dirty_ids[state.current_idx];
3626            let cache_result = self.page_cache.write().get(&PageCacheKey::new(page_id))?;
3627
3628            match cache_result {
3629                Some(page) => {
3630                    trace!(
3631                        "cacheflush(page={}, page_type={:?})",
3632                        page_id,
3633                        page.get_contents().page_type().ok()
3634                    );
3635                    state.collected_pages.push(page);
3636                    state.current_idx += 1;
3637                }
3638                None => {
3639                    // Page evicted, need async read from WAL
3640                    trace!("cacheflush: page {} evicted, reading from WAL", page_id);
3641                    let (page, completion) =
3642                        self.read_page_no_cache(page_id as i64, None, false)?;
3643
3644                    if !completion.succeeded() {
3645                        return Ok(CacheFlushStep::Yield(
3646                            CacheFlushState::WaitingForRead {
3647                                state,
3648                                page_id,
3649                                page,
3650                                completion: completion.clone(),
3651                            },
3652                            IOCompletions::Single(completion),
3653                        ));
3654                    }
3655
3656                    // Sync read completed immediately
3657                    trace!(
3658                        "cacheflush(page={}, page_type={:?}) [re-read sync]",
3659                        page_id,
3660                        page.get_contents().page_type().ok()
3661                    );
3662                    state.collected_pages.push(page);
3663                    state.current_idx += 1;
3664                }
3665            }
3666            if Self::should_flush_batch(&state) {
3667                self.flush_page_batch(wal, page_sz, &mut state)?;
3668            }
3669        }
3670        // All pages collected and written
3671        Ok(CacheFlushStep::Done(state.completions))
3672    }
3673
3674    /// Handle completion of async page read for evicted page.
3675    fn cacheflush_handle_read(
3676        &self,
3677        wal: &Arc<dyn Wal>,
3678        page_sz: PageSize,
3679        mut state: CollectingState,
3680        page_id: usize,
3681        page: PageRef,
3682        completion: Completion,
3683    ) -> Result<CacheFlushStep> {
3684        if !completion.succeeded() {
3685            return Ok(CacheFlushStep::Yield(
3686                CacheFlushState::WaitingForRead {
3687                    state,
3688                    page_id,
3689                    page,
3690                    completion: completion.clone(),
3691                },
3692                IOCompletions::Single(completion),
3693            ));
3694        }
3695        trace!(
3696            "cacheflush(page={}, page_type={:?}) [re-read complete]",
3697            page_id,
3698            page.get_contents().page_type().ok()
3699        );
3700        state.collected_pages.push(page);
3701        state.current_idx += 1;
3702        if Self::should_flush_batch(&state) {
3703            self.flush_page_batch(wal, page_sz, &mut state)?;
3704        }
3705
3706        Ok(CacheFlushStep::Continue(CacheFlushState::Collecting(state)))
3707    }
3708
3709    #[inline]
3710    fn should_flush_batch(state: &CollectingState) -> bool {
3711        let at_capacity = state.collected_pages.len() == IOV_MAX;
3712        let at_end = state.current_idx >= state.dirty_ids.len();
3713        !state.collected_pages.is_empty() && (at_capacity || at_end)
3714    }
3715
3716    /// Writes accumulated pages to WAL as a single vectored append.
3717    #[inline]
3718    fn flush_page_batch(
3719        &self,
3720        wal: &Arc<dyn Wal>,
3721        page_sz: PageSize,
3722        state: &mut CollectingState,
3723    ) -> Result<()> {
3724        let pages = std::mem::take(&mut state.collected_pages);
3725        // Mark pages as write-pending to detect concurrent modifications
3726        for page in &pages {
3727            page.set_write_pending();
3728        }
3729        match wal.append_frames_vectored(pages, page_sz) {
3730            Ok(completion) => {
3731                state.completions.push(completion);
3732                Ok(())
3733            }
3734            Err(e) => {
3735                self.io.cancel(&state.completions)?;
3736                self.io.drain_completions(&state.completions)?;
3737                Err(e)
3738            }
3739        }
3740    }
3741
3742    /// Attempt to spill dirty pages from the cache to make room for new pages.
3743    /// This is called when the cache reaches its spill threshold.
3744    ///
3745    /// For databases with a WAL: write only spillable dirty pages to WAL,
3746    /// then mark them as spilled so they can be evicted even while dirty.
3747    /// For ephemeral tables: writes pages directly to the temp database file.
3748    #[instrument(skip_all, level = Level::DEBUG)]
3749    fn try_spill_dirty_pages(&self) -> Result<IOResult<()>> {
3750        loop {
3751            let state = self.spill_state.read().clone();
3752            match state {
3753                SpillState::Idle => {
3754                    // Check if spilling is needed
3755                    let spill_result = {
3756                        let cache = self.page_cache.read();
3757                        cache.check_spill(IOV_MAX)
3758                    };
3759                    match spill_result {
3760                        SpillResult::NotNeeded | SpillResult::Disabled => {
3761                            return Ok(IOResult::Done(()));
3762                        }
3763                        SpillResult::CacheFull => {
3764                            tracing::debug!(
3765                                "try_spill_dirty_pages: cache full, no spillable pages"
3766                            );
3767                            return Ok(IOResult::Done(()));
3768                        }
3769                        SpillResult::PagesToSpill(pages) => {
3770                            if pages.is_empty() {
3771                                return Ok(IOResult::Done(()));
3772                            }
3773                            let page_count = pages.len();
3774                            tracing::debug!("try_spill_dirty_pages: spilling {} pages", page_count);
3775                            if let Some(wal) = self.wal.as_ref() {
3776                                let page_sz = self.get_page_size().unwrap_or_default();
3777
3778                                // Ensure WAL is initialized. Most of the time this
3779                                // is a no-op (returns None). When it does require
3780                                // IO we transition through `PreparingWalStart` /
3781                                // `PreparingWalFinish` and yield rather than block,
3782                                // carrying the pinned `pages` across each yield.
3783                                match wal.prepare_wal_start(page_sz)? {
3784                                    Some(c) => {
3785                                        *self.spill_state.write() = SpillState::PreparingWalStart {
3786                                            pages,
3787                                            completion: c,
3788                                        };
3789                                        // Loop to handle the new state (which will
3790                                        // yield if the completion isn't finished).
3791                                        continue;
3792                                    }
3793                                    None => {
3794                                        // WAL already initialized — append directly.
3795                                        return self.spill_append_frames_to_wal(pages);
3796                                    }
3797                                }
3798                            } else {
3799                                let mut group = CompletionGroup::new(|_| {});
3800                                // Ephemeral table case: write directly to temp file
3801                                for page in &pages {
3802                                    page.set_write_pending();
3803                                }
3804                                let completions = self.spill_pages_to_disk(&pages)?;
3805                                if completions.is_empty() {
3806                                    self.finish_ephemeral_spill(&pages);
3807                                    return Ok(IOResult::Done(()));
3808                                }
3809                                for completion in &completions {
3810                                    group.add(completion);
3811                                }
3812                                *self.spill_state.write() = SpillState::WritingToDisk {
3813                                    pages,
3814                                    completions: completions.clone(),
3815                                };
3816                                io_yield_one!(group.build());
3817                            }
3818                        }
3819                    }
3820                }
3821                SpillState::PreparingWalStart { pages, completion } => {
3822                    if !completion.succeeded() {
3823                        io_yield_one!(completion);
3824                    }
3825                    // Header (and any truncate) durable — issue the fsync that
3826                    // marks the WAL initialized.
3827                    let wal = self.wal.as_ref().expect("PreparingWalStart requires a WAL");
3828                    let finish_c = wal.prepare_wal_finish(self.get_sync_type())?;
3829                    *self.spill_state.write() = SpillState::PreparingWalFinish {
3830                        pages,
3831                        completion: finish_c,
3832                    };
3833                    continue;
3834                }
3835                SpillState::PreparingWalFinish { pages, completion } => {
3836                    if !completion.succeeded() {
3837                        io_yield_one!(completion);
3838                    }
3839                    // WAL is now initialized; append the spill frames.
3840                    return self.spill_append_frames_to_wal(pages);
3841                }
3842                SpillState::WritingToWal { pages, completions } => {
3843                    for c in &completions {
3844                        if !c.succeeded() {
3845                            io_yield_one!(c.clone());
3846                        }
3847                    }
3848                    // All I/O complete, pages are now in WAL.
3849                    // Mark spilled pages so they can be evicted while dirty.
3850                    // Only do so if page wasn't modified since write started (each page has valid wal_tag).
3851                    let mut spilled_count = 0;
3852                    {
3853                        let mut cache = self.page_cache.write();
3854                        for page in &pages {
3855                            if page.has_wal_tag() {
3856                                let key = PageCacheKey::new(page.get().id);
3857                                cache.notify_page_spilled(key);
3858                                page.set_spilled();
3859                                spilled_count += 1;
3860                            } else {
3861                                // Page was modified during write, it will need to be re-spilled
3862                                tracing::debug!(
3863                                "try_spill_dirty_pages: page {} modified during write, not marking as spilled",
3864                                page.get().id
3865                            );
3866                            }
3867                        }
3868                    }
3869                    if spilled_count == 0 && !pages.is_empty() {
3870                        tracing::warn!(
3871                        "try_spill_dirty_pages: no pages marked as spilled out of {}, all were modified during write",
3872                        pages.len()
3873                    );
3874                    }
3875                    *self.spill_state.write() = SpillState::Idle;
3876                    trace!(
3877                        "try_spill_dirty_pages: successfully spilled {} / {} pages to WAL",
3878                        spilled_count,
3879                        pages.len(),
3880                    );
3881                    return Ok(IOResult::Done(()));
3882                }
3883                SpillState::WritingToDisk { pages, completions } => {
3884                    let all_done = completions.iter().all(|c| c.succeeded());
3885                    if !all_done {
3886                        for c in &completions {
3887                            if !c.succeeded() {
3888                                io_yield_one!(c.clone());
3889                            }
3890                        }
3891                    }
3892                    // All I/O complete, finish ephemeral spill
3893                    self.finish_ephemeral_spill(&pages);
3894                    *self.spill_state.write() = SpillState::Idle;
3895                    trace!(
3896                        "try_spill_dirty_pages: successfully spilled {} pages to disk",
3897                        pages.len()
3898                    );
3899                    return Ok(IOResult::Done(()));
3900                }
3901            }
3902        }
3903    }
3904
3905    /// Append the prepared spill `pages` as WAL frames. Returns `Done` if
3906    /// the write completed synchronously, otherwise transitions to
3907    /// `SpillState::WritingToWal` and yields the write completion. The WAL
3908    /// must already be initialized (callers route through `PreparingWal*`
3909    /// first).
3910    fn spill_append_frames_to_wal(&self, pages: Vec<PinGuard>) -> Result<IOResult<()>> {
3911        let wal = self
3912            .wal
3913            .as_ref()
3914            .expect("spill_append_frames_to_wal requires a WAL");
3915        let page_sz = self.get_page_size().unwrap_or_default();
3916        let wal_pages: Vec<PageRef> = pages
3917            .iter()
3918            .map(|p| -> Result<PageRef> {
3919                self.subjournal_page_if_required(p)?;
3920                // Set write_pending on all pages before WAL write so callback can
3921                // detect mid-write modifications.
3922                p.set_write_pending();
3923                Ok(p.to_page())
3924            })
3925            .collect::<Result<Vec<_>>>()?;
3926        let c = wal.append_frames_vectored(wal_pages, page_sz)?;
3927
3928        if c.succeeded() {
3929            // Synchronous completion, WAL tags already set by callback.
3930            {
3931                let mut cache = self.page_cache.write();
3932                for page in &pages {
3933                    if page.has_wal_tag() {
3934                        let key = PageCacheKey::new(page.get().id);
3935                        cache.notify_page_spilled(key);
3936                        page.set_spilled();
3937                    }
3938                }
3939            }
3940            *self.spill_state.write() = SpillState::Idle;
3941            return Ok(IOResult::Done(()));
3942        }
3943        *self.spill_state.write() = SpillState::WritingToWal {
3944            pages,
3945            completions: vec![c.clone()],
3946        };
3947        io_yield_one!(c);
3948    }
3949
3950    /// Wait for any in-flight spill writes to finish.
3951    /// This prevents publishing WAL metadata that references frames that are not yet durable.
3952    fn wait_for_spill_completions(&self) -> Result<IOResult<()>> {
3953        loop {
3954            let state = self.spill_state.read().clone();
3955            if matches!(state, SpillState::Idle) {
3956                return Ok(IOResult::Done(()));
3957            }
3958            match self.try_spill_dirty_pages()? {
3959                IOResult::Done(()) => continue,
3960                IOResult::IO(c) => return Ok(IOResult::IO(c)),
3961            }
3962        }
3963    }
3964
3965    /// Finish a spill operation for ephemeral tables
3966    fn finish_ephemeral_spill(&self, pages: &[PinGuard]) {
3967        for page in pages {
3968            let tag = page.get().wal_tag.load(Ordering::Acquire);
3969            // wal tag is set to TAG_UNSET when adding to dirty_pages, meaning that this
3970            // page was dirtied after the spill started, so we don't clear the dirty flag in that case
3971            if tag != TAG_UNSET {
3972                page.clear_dirty();
3973            }
3974        }
3975    }
3976    /// Write a set of pages directly to the database file (for ephemeral tables without WAL).
3977    /// This is used by try_spill_dirty_pages for ephemeral tables/indexes.
3978    fn spill_pages_to_disk(&self, pages: &[PinGuard]) -> Result<Vec<Completion>> {
3979        let mut completions: Vec<Completion> = Vec::with_capacity(pages.len());
3980        for page in pages {
3981            match begin_write_btree_page(self, &page.to_page()) {
3982                Ok(c) => completions.push(c),
3983                Err(e) => {
3984                    self.io.cancel(&completions)?;
3985                    self.io.drain_completions(&completions)?;
3986                    return Err(e);
3987                }
3988            }
3989        }
3990
3991        Ok(completions)
3992    }
3993
3994    /// Check if the cache needs spilling and attempt to spill if necessary.
3995    /// This should be called before inserting new pages into the cache.
3996    fn ensure_cache_space(&self) -> Result<IOResult<()>> {
3997        let needs_spill = {
3998            let cache = self.page_cache.read();
3999            cache.needs_spill()
4000        };
4001
4002        if needs_spill {
4003            match self.try_spill_dirty_pages()? {
4004                IOResult::Done(()) => {
4005                    // Whether or not anything could be spilled, proceed: the
4006                    // capacity is a soft limit, and the upcoming insert
4007                    // evicts what it can and admits the page over capacity
4008                    // otherwise.
4009                }
4010                IOResult::IO(completion) => {
4011                    return Ok(IOResult::IO(completion));
4012                }
4013            }
4014        }
4015        Ok(IOResult::Done(()))
4016    }
4017
4018    /// Commit the write transaction to the WAL: write any dirty pages as WAL
4019    /// frames, fsync the WAL if it is dirty, and publish the commit. The WAL
4020    /// can be dirty without any dirty pages (frames inserted through
4021    /// `write_frame_raw` bypass dirty-page tracking), so under
4022    /// synchronous=FULL this fsyncs even when there is nothing to write.
4023    /// If the WAL size is over the checkpoint threshold, it will checkpoint the WAL to
4024    /// the database file and then fsync the database file.
4025    ///
4026    /// `allowed_auto_actions` controls automatic WAL maintenance permitted at
4027    /// commit time. Only `WalAutoActions::Checkpoint` is consulted here — it
4028    /// gates the post-commit auto-checkpoint when `should_checkpoint()` is
4029    /// true.
4030    #[instrument(skip_all, level = Level::DEBUG)]
4031    pub fn commit_wal(
4032        &self,
4033        allowed_auto_actions: WalAutoActions,
4034        sync_mode: SyncMode,
4035        data_sync_retry: bool,
4036    ) -> Result<IOResult<()>> {
4037        {
4038            let mut commit_info = self.commit_info.write();
4039            if commit_info.state == CommitState::PrepareWal {
4040                commit_info.reset();
4041            }
4042        }
4043
4044        // Wait for spill writes before publishing frames
4045        if let IOResult::IO(c) = self.wait_for_spill_completions()? {
4046            return Ok(IOResult::IO(c));
4047        }
4048
4049        let result = self.commit_wal_inner(allowed_auto_actions, sync_mode, data_sync_retry);
4050        if result.is_err() {
4051            self.commit_info.write().reset();
4052        }
4053        result
4054    }
4055
4056    pub fn commit_wal_end(&self) {
4057        self.commit_info.write().reset();
4058    }
4059
4060    #[instrument(skip_all, level = Level::DEBUG)]
4061    #[aristo::intent("A commit frame must reach stable storage via fsync before the transaction is reported as durable\n", id = "aristos:wal_commit_requires_fsync", verify = "full", parent = "wal_protocol_correctness")]
4062    fn commit_wal_inner(
4063        &self,
4064        allowed_auto_actions: WalAutoActions,
4065        sync_mode: SyncMode,
4066        data_sync_retry: bool,
4067    ) -> Result<IOResult<()>> {
4068        let Some(wal) = self.wal.as_ref() else {
4069            turso_soft_unreachable!("commit_wal() called without WAL");
4070            return Err(LimboError::InternalError(
4071                "commit_wal() called without WAL".into(),
4072            ));
4073        };
4074
4075        loop {
4076            let state = self.commit_info.read().state;
4077            trace!(?state);
4078
4079            match state {
4080                CommitState::PrepareWal => {
4081                    let page_sz = self.get_page_size_unchecked();
4082                    let c = wal.prepare_wal_start(page_sz)?;
4083                    let Some(c) = c else {
4084                        self.commit_info.write().state = CommitState::GetDbSize;
4085                        continue;
4086                    };
4087                    self.commit_info.write().state = CommitState::PrepareWalSync;
4088                    if !c.succeeded() {
4089                        io_yield_one!(c);
4090                    }
4091                }
4092                CommitState::PrepareWalSync => {
4093                    let c = wal.prepare_wal_finish(self.get_sync_type())?;
4094                    self.commit_info.write().state = CommitState::GetDbSize;
4095                    if !c.succeeded() {
4096                        io_yield_one!(c);
4097                    }
4098                }
4099                CommitState::GetDbSize => {
4100                    let db_size = return_if_io!(self.with_header(|h| h.database_size));
4101                    self.commit_info.write().state = CommitState::ScanAndIssueReads {
4102                        db_size: db_size.get(),
4103                    };
4104                }
4105                CommitState::ScanAndIssueReads { db_size } => {
4106                    let mut commit_info = self.commit_info.write();
4107                    let dirty_pages = self.dirty_pages.read();
4108
4109                    if dirty_pages.is_empty() {
4110                        // No dirty pages to flush, but that does not mean the
4111                        // WAL is clean: frames written through
4112                        // write_frame_raw() bypass dirty-page tracking, and
4113                        // callers (e.g. the sync engine ending a raw-insert
4114                        // session) treat this commit as their durability
4115                        // barrier. WaitSync fsyncs if the WAL is dirty.
4116                        commit_info.state = CommitState::WaitSync;
4117                        continue;
4118                    }
4119                    commit_info.initialize(dirty_pages.len() as usize);
4120                    let mut cache = self.page_cache.write();
4121
4122                    for page_id in dirty_pages.iter() {
4123                        let page_id = page_id as usize;
4124                        let page_key = PageCacheKey::new(page_id);
4125                        if cache.peek(&page_key, false).is_some() {
4126                            commit_info.page_sources.push(PageSource::Cached(page_id));
4127                        } else {
4128                            let (page, completion) =
4129                                self.read_page_no_cache(page_id as i64, None, false)?;
4130                            // If the read completed synchronously with an error,
4131                            // surface it now. Otherwise we would silently drop the
4132                            // failure (the completion is "finished" so we'd skip
4133                            // pushing it into the wait list) and later trip the
4134                            // page-buffer-not-loaded panic in prepare_frames when
4135                            // it tries to read content from the evicted page.
4136                            if completion.finished() && !completion.succeeded() {
4137                                let err = completion.get_error().unwrap_or(
4138                                    CompletionError::IOError(std::io::ErrorKind::Other, "read"),
4139                                );
4140                                return Err(LimboError::CompletionError(err));
4141                            }
4142                            commit_info.page_sources.push(PageSource::Evicted(page));
4143                            if !completion.finished() {
4144                                commit_info.completions.push(completion);
4145                            }
4146                        }
4147                    }
4148                    drop(cache);
4149                    drop(dirty_pages);
4150                    if !commit_info.completions.is_empty() {
4151                        commit_info.state = CommitState::WaitBatchedReads { db_size };
4152                        drop(commit_info);
4153                        io_yield_one!(self.commit_completion());
4154                    }
4155                    commit_info.state = CommitState::PrepareFrames { db_size };
4156                }
4157                CommitState::WaitBatchedReads { db_size } => {
4158                    let all_done = self
4159                        .commit_info
4160                        .read()
4161                        .completions
4162                        .iter()
4163                        .all(|c| c.finished());
4164                    if !all_done {
4165                        io_yield_one!(self.commit_completion());
4166                    }
4167                    // Check for any read errors
4168                    let mut commit_info = self.commit_info.write();
4169                    let failed = commit_info
4170                        .completions
4171                        .iter()
4172                        .find(|c| !c.succeeded())
4173                        .cloned();
4174                    if let Some(_failed) = failed {
4175                        return Err(LimboError::CompletionError(CompletionError::IOError(
4176                            std::io::ErrorKind::Other,
4177                            "read",
4178                        )));
4179                    }
4180                    // All reads complete and successful, proceed to frame preparation
4181                    commit_info.completions.clear();
4182                    commit_info.completion_group = None;
4183                    commit_info.state = CommitState::PrepareFrames { db_size };
4184                }
4185                CommitState::PrepareFrames { db_size } => {
4186                    let page_sz = self.get_page_size_unchecked();
4187                    let mut commit_info = self.commit_info.write();
4188                    let mut cache = self.page_cache.write();
4189
4190                    'inner: loop {
4191                        let cursor = commit_info.page_source_cursor;
4192                        if cursor >= commit_info.page_sources.len() {
4193                            break 'inner;
4194                        }
4195
4196                        let total = commit_info.page_sources.len();
4197                        let is_last = cursor + 1 >= total;
4198                        // Linear consumption, no lookup required
4199                        let page = match &commit_info.page_sources[cursor] {
4200                            PageSource::Cached(page_id) => {
4201                                let page_key = PageCacheKey::new(*page_id);
4202                                cache
4203                                    .get(&page_key)?
4204                                    .expect("page evicted between scan and prepare")
4205                            }
4206                            PageSource::Evicted(page) => page.clone(),
4207                        };
4208                        // Defensive check: prepare_frames will read page contents,
4209                        // which panics if the buffer is not loaded. If we got here
4210                        // with an unloaded page (e.g. an evicted dirty page whose
4211                        // backing WAL frame was truncated by a savepoint rollback),
4212                        // surface an internal error instead of panicking.
4213                        if !page.is_loaded() {
4214                            return Err(LimboError::InternalError(format!(
4215                                "dirty page {} has no buffer loaded at commit time",
4216                                page.get().id
4217                            )));
4218                        }
4219                        turso_assert!(
4220                            page.get().overflow_cells.is_empty(),
4221                            "dirty page still has overflow cells at commit time",
4222                            { "page_id": page.get().id }
4223                        );
4224                        commit_info.page_source_cursor += 1;
4225                        commit_info.collected_pages.push(page);
4226
4227                        if commit_info.collected_pages.len() == IOV_MAX || is_last {
4228                            self.prepare_collected_frames(
4229                                &mut commit_info,
4230                                wal,
4231                                page_sz,
4232                                db_size,
4233                                is_last,
4234                            )?;
4235                        }
4236                    }
4237                    drop(cache);
4238                    if commit_info.prepared_frames.is_empty() {
4239                        turso_assert!(
4240                            self.dirty_pages.read().is_empty(),
4241                            "dirty pages must be empty if no frames prepared"
4242                        );
4243                        return Ok(IOResult::Done(()));
4244                    }
4245                    // Submit all WAL writes
4246                    let wal_file = wal.wal_file()?;
4247                    let mut batch = WriteBatch::new(wal_file);
4248                    for prepared in &commit_info.prepared_frames {
4249                        batch.writev(prepared.offset, &prepared.bufs);
4250                    }
4251                    commit_info.completions = batch.submit()?;
4252                    commit_info.completion_group = None;
4253                    commit_info.state = CommitState::WaitWrites;
4254                }
4255                CommitState::WaitWrites => {
4256                    if !self
4257                        .commit_info
4258                        .read()
4259                        .completions
4260                        .iter()
4261                        .all(|c| c.finished())
4262                    {
4263                        io_yield_one!(self.commit_completion());
4264                    }
4265                    // Check for any write errors
4266                    let failed = self
4267                        .commit_info
4268                        .read()
4269                        .completions
4270                        .iter()
4271                        .find(|c| !c.succeeded())
4272                        .cloned();
4273
4274                    let mut commit_info = self.commit_info.write();
4275                    if let Some(_failed) = failed {
4276                        commit_info.completions.clear();
4277                        commit_info.completion_group = None;
4278                        commit_info.prepared_frames.clear();
4279                        return Err(LimboError::CompletionError(CompletionError::IOError(
4280                            std::io::ErrorKind::Other,
4281                            "write",
4282                        )));
4283                    }
4284                    commit_info.completions.clear();
4285                    commit_info.completion_group = None;
4286                    // All writes complete; WaitSync submits the WAL fsync if
4287                    // one is owed.
4288                    commit_info.state = CommitState::WaitSync;
4289                }
4290                // To protect against partial writes, we MUST ensure that all write Completions
4291                // finish before submitting the fsync. It is possible that a partial write will
4292                // cause an IO backend to resubmit the write (particularly with io_uring) and we
4293                // cannot have the fsync submitted before all writes are fully done, even if
4294                // they are IO_LINK'd together or we submit the fsync with IO_DRAIN, the only way
4295                // to ensure durability in the case of partial writes is to ensure the pwritev
4296                // completes before the fsync is submitted.
4297                CommitState::WaitSync => {
4298                    // A pending completion means a previous entry into this
4299                    // state already submitted the fsync; wait on it instead
4300                    // of submitting a second one. At most one fsync is ever in
4301                    // flight, so completions holds either the pending fsync or
4302                    // nothing.
4303                    assert!(
4304                        self.commit_info.read().completions.len() <= 1,
4305                        "WaitSync expects at most one in-flight fsync completion"
4306                    );
4307                    let pending = self.commit_info.read().completions.first().cloned();
4308                    let sync_c = match pending {
4309                        Some(c) => Some(c),
4310                        // Skip the fsync when the WAL is not dirty (no frames
4311                        // appended since the last successful fsync).
4312                        // NORMAL mode skips fsync on WAL commit (but still
4313                        // fsyncs on checkpoint and wal restart).
4314                        None if sync_mode == SyncMode::Full && wal.is_dirty() => {
4315                            let sync_c = wal.sync(self.get_sync_type())?;
4316                            self.commit_info.write().completions.push(sync_c.clone());
4317                            Some(sync_c)
4318                        }
4319                        None => None,
4320                    };
4321                    if let Some(sync_c) = sync_c {
4322                        // Wait for fsync to complete
4323                        if !sync_c.finished() {
4324                            io_yield_one!(sync_c);
4325                        }
4326                        // Check for fsync error as we might need to panic on data_sync_retry=off
4327                        let mut commit_info = self.commit_info.write();
4328                        if !sync_c.succeeded() {
4329                            commit_info.completions.clear();
4330                            commit_info.prepared_frames.clear();
4331
4332                            if !data_sync_retry {
4333                                panic!(
4334                                    "fsync error (data_sync_retry=off): {:?}",
4335                                    sync_c.get_error()
4336                                );
4337                            }
4338                            return Err(LimboError::CompletionError(CompletionError::IOError(
4339                                std::io::ErrorKind::Other,
4340                                "sync",
4341                            )));
4342                        }
4343                        commit_info.completions.clear();
4344                    }
4345                    let mut commit_info = self.commit_info.write();
4346                    if commit_info.prepared_frames.is_empty() {
4347                        // Nothing to publish: the frames this fsync covered
4348                        // published themselves via finish_append_frames_commit()
4349                        // when they were appended.
4350                        return Ok(IOResult::Done(()));
4351                    }
4352                    commit_info.state = CommitState::WalCommitDone;
4353                }
4354                CommitState::WalCommitDone => {
4355                    // all I/O complete, NOW it's safe to advance WAL state
4356                    let mut commit_info = self.commit_info.write();
4357                    wal.commit_prepared_frames(&commit_info.prepared_frames);
4358                    wal.finalize_committed_pages(&commit_info.prepared_frames);
4359                    wal.finish_append_frames_commit()?;
4360                    self.dirty_pages.write().clear();
4361                    commit_info.prepared_frames.clear();
4362
4363                    let need_checkpoint = allowed_auto_actions.contains(WalAutoActions::Checkpoint)
4364                        && wal.should_checkpoint();
4365                    if need_checkpoint {
4366                        commit_info.state = CommitState::AutoCheckpoint;
4367                    }
4368                    return Ok(IOResult::Done(()));
4369                }
4370                CommitState::AutoCheckpoint => panic!("checkpoint must be handled externally"),
4371            }
4372        }
4373    }
4374
4375    /// Prepare collected pages as WAL frames without submitting I/O.
4376    fn prepare_collected_frames(
4377        &self,
4378        commit_info: &mut CommitInfo,
4379        wal: &Arc<dyn Wal>,
4380        page_sz: PageSize,
4381        db_size: u32,
4382        is_commit_frame: bool,
4383    ) -> Result<()> {
4384        let pages = std::mem::take(&mut commit_info.collected_pages);
4385        if pages.is_empty() {
4386            return Ok(());
4387        }
4388        let commit_flag = if is_commit_frame { Some(db_size) } else { None };
4389        for page in &pages {
4390            page.set_write_pending();
4391        }
4392        // Chain from previous batch if any
4393        let prev = commit_info.prepared_frames.last();
4394        let prepared = wal.prepare_frames(&pages, page_sz, commit_flag, prev)?;
4395        tracing::debug!("prepare_collected_frames: offset={}", prepared.offset);
4396        commit_info.prepared_frames.push(prepared);
4397        Ok(())
4398    }
4399
4400    fn commit_completion(&self) -> Completion {
4401        let mut commit_info = self.commit_info.write();
4402        if let Some(group) = &commit_info.completion_group {
4403            return group.clone();
4404        }
4405        let mut group = CompletionGroup::new(|_| {});
4406        for c in commit_info.completions.iter() {
4407            group.add(c);
4408        }
4409        let result = group.build();
4410        commit_info.completion_group = Some(result.clone());
4411        result
4412    }
4413
4414    #[instrument(skip_all, level = Level::DEBUG)]
4415    pub fn wal_changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>> {
4416        let wal = self.wal.as_ref().unwrap();
4417        wal.changed_pages_after(frame_watermark)
4418    }
4419
4420    #[instrument(skip_all, level = Level::DEBUG)]
4421    pub fn wal_get_frame(&self, frame_no: u64, frame: &mut [u8]) -> Result<Completion> {
4422        let Some(wal) = self.wal.as_ref() else {
4423            turso_soft_unreachable!("wal_get_frame() called on database without WAL");
4424            return Err(LimboError::InternalError(
4425                "wal_get_frame() called on database without WAL".to_string(),
4426            ));
4427        };
4428        wal.read_frame_raw(frame_no, frame)
4429    }
4430
4431    #[instrument(skip_all, level = Level::DEBUG)]
4432    pub fn wal_insert_frame(&self, frame_no: u64, frame: &[u8]) -> Result<WalFrameInfo> {
4433        let Some(wal) = self.wal.as_ref() else {
4434            turso_soft_unreachable!("wal_insert_frame() called on database without WAL");
4435            return Err(LimboError::InternalError(
4436                "wal_insert_frame() called on database without WAL".to_string(),
4437            ));
4438        };
4439        let (header, raw_page) = parse_wal_frame_header(frame);
4440
4441        wal.write_frame_raw(
4442            self.buffer_pool.clone(),
4443            frame_no,
4444            header.page_number as u64,
4445            header.db_size as u64,
4446            raw_page,
4447            self.get_sync_type(),
4448        )?;
4449        if let Some(page) = self.cache_get(header.page_number as usize)? {
4450            let content = page.get_contents();
4451            content.as_ptr().copy_from_slice(raw_page);
4452            turso_assert!(
4453                page.get().id == header.page_number as usize,
4454                "page has unexpected id"
4455            );
4456        }
4457        if header.page_number == 1 {
4458            let db_size = self
4459                .io
4460                .block(|| self.with_header(|header| header.database_size))?;
4461            tracing::debug!("truncate page_cache as first page was written: {}", db_size);
4462            let mut page_cache = self.page_cache.write();
4463            page_cache.truncate(db_size.get() as usize).map_err(|e| {
4464                LimboError::InternalError(format!("Failed to truncate page cache: {e:?}"))
4465            })?;
4466        }
4467        if header.is_commit_frame() {
4468            let mut dirty_pages = self.dirty_pages.write();
4469            tracing::debug!(
4470                "wal_callback: commit frame, clearing {} dirty pages",
4471                dirty_pages.len()
4472            );
4473            let mut cache = self.page_cache.write();
4474            for page_id in dirty_pages.iter() {
4475                let page_key = PageCacheKey::new(page_id as usize);
4476                // Page may have been evicted from cache after spilling to WAL
4477                if let Some(page) = cache.get(&page_key)? {
4478                    page.clear_dirty();
4479                }
4480            }
4481            dirty_pages.clear();
4482        }
4483        Ok(WalFrameInfo {
4484            page_no: header.page_number,
4485            db_size: header.db_size,
4486        })
4487    }
4488
4489    pub fn is_checkpointing(&self) -> bool {
4490        !matches!(
4491            self.checkpoint_state.read().phase.clone(),
4492            CheckpointPhase::NotCheckpointing
4493        )
4494    }
4495
4496    fn reset_checkpoint_state(&self) {
4497        self.clear_checkpoint_state();
4498        self.commit_info.write().state = CommitState::PrepareWal;
4499    }
4500
4501    /// Reset checkpoint state machine to initial state.
4502    /// Use this to clean up after a failed explicit checkpoint (PRAGMA wal_checkpoint).
4503    pub fn clear_checkpoint_state(&self) {
4504        let mut state = self.checkpoint_state.write();
4505        state.phase = CheckpointPhase::NotCheckpointing;
4506        state.result = None;
4507        state.mode = None;
4508        state.lock_source = CheckpointLockSource::Acquire;
4509    }
4510
4511    /// Clean up after a auto-checkpoint failure.
4512    /// Auto-checkpoint executed outside of the main transaction - so WAL transaction was already finalized
4513    pub fn cleanup_after_auto_checkpoint_failure(&self) {
4514        self.cleanup_after_checkpoint_failure();
4515    }
4516
4517    pub fn cleanup_after_checkpoint_failure(&self) {
4518        self.reset_checkpoint_state();
4519        if let Some(wal) = self.wal.as_ref() {
4520            wal.abort_checkpoint();
4521        }
4522    }
4523
4524    fn next_post_sync_checkpoint_phase(&self, clear_page_cache: bool) -> CheckpointPhase {
4525        let state = self.checkpoint_state.read();
4526        let result = state.result.as_ref().expect("result should be set");
4527        let mode = state.mode.expect("mode should be set");
4528        if result.wal_checkpoint_backfilled > 0
4529            && !matches!(
4530                mode,
4531                CheckpointMode::Restart | CheckpointMode::Truncate { .. }
4532            )
4533        {
4534            return CheckpointPhase::ReadDbIdentity {
4535                clear_page_cache,
4536                read: PendingCheckpointDbIdentityRead {
4537                    max_frame: result.wal_total_backfilled,
4538                    header_buf: Arc::new(Buffer::new_temporary(PageSize::MIN as usize)),
4539                    bytes_read: Arc::new(AtomicUsize::new(usize::MAX)),
4540                    read_sent: false,
4541                },
4542            };
4543        }
4544        if matches!(mode, CheckpointMode::Truncate { .. }) {
4545            CheckpointPhase::TruncateWalFile { clear_page_cache }
4546        } else {
4547            CheckpointPhase::Finalize { clear_page_cache }
4548        }
4549    }
4550
4551    #[instrument(skip_all, level = Level::DEBUG, name = "pager_checkpoint",)]
4552    /// Checkpoint the WAL to the database file (if needed).
4553    /// Args:
4554    /// - mode: The checkpoint mode to use (PASSIVE, FULL, RESTART, TRUNCATE)
4555    /// - sync_mode: The fsync mode to use (OFF, NORMAL, FULL)
4556    /// - clear_page_cache: Whether to clear the page cache after checkpointing
4557    pub fn checkpoint(
4558        &self,
4559        mode: CheckpointMode,
4560        sync_mode: crate::SyncMode,
4561        clear_page_cache: bool,
4562    ) -> Result<IOResult<CheckpointResult>> {
4563        self.checkpoint_inner(
4564            mode,
4565            sync_mode,
4566            clear_page_cache,
4567            CheckpointLockSource::Acquire,
4568        )
4569    }
4570
4571    pub fn vacuum_checkpoint_with_held_lock(
4572        &self,
4573        sync_mode: crate::SyncMode,
4574        clear_page_cache: bool,
4575    ) -> Result<IOResult<CheckpointResult>> {
4576        self.checkpoint_inner(
4577            CheckpointMode::Truncate {
4578                upper_bound_inclusive: None,
4579            },
4580            sync_mode,
4581            clear_page_cache,
4582            CheckpointLockSource::HeldByCaller,
4583        )
4584    }
4585
4586    #[aristo::intent("The nbackfills counter advances after frames are durable, so recovery never replays already-checkpointed frames\n", id = "aristos:wal_nbackfills_orders_with_recovery", verify = "full", parent = "wal_protocol_correctness")]
4587    fn checkpoint_inner(
4588        &self,
4589        mode: CheckpointMode,
4590        sync_mode: crate::SyncMode,
4591        clear_page_cache: bool,
4592        lock_source: CheckpointLockSource,
4593    ) -> Result<IOResult<CheckpointResult>> {
4594        let Some(wal) = self.wal.as_ref() else {
4595            turso_soft_unreachable!("checkpoint() called on database without WAL");
4596            return Err(LimboError::InternalError(
4597                "checkpoint() called on database without WAL".to_string(),
4598            ));
4599        };
4600        loop {
4601            // Clone the phase to check what state we're in, but keep result in place
4602            // This is important because we need to be careful not to e.g. clone and drop the checkpoint result which
4603            // causes a drop of CheckpointLocks prematurely and results in a panic.
4604            let phase = self.checkpoint_state.read().phase.clone();
4605            match phase {
4606                CheckpointPhase::NotCheckpointing => {
4607                    let mut state = self.checkpoint_state.write();
4608                    state.phase = CheckpointPhase::Checkpoint {
4609                        mode,
4610                        sync_mode,
4611                        clear_page_cache,
4612                    };
4613                    state.mode = Some(mode);
4614                    state.lock_source = lock_source;
4615                }
4616                CheckpointPhase::Checkpoint {
4617                    mode,
4618                    sync_mode,
4619                    clear_page_cache,
4620                } => {
4621                    let checkpoint_lock_source = self.checkpoint_state.read().lock_source;
4622                    let res = return_if_io!(match checkpoint_lock_source {
4623                        CheckpointLockSource::Acquire => wal.checkpoint(self, mode),
4624                        CheckpointLockSource::HeldByCaller => {
4625                            wal.vacuum_checkpoint_with_held_lock(self)
4626                        }
4627                    });
4628                    let mut state = self.checkpoint_state.write();
4629                    if matches!(mode, CheckpointMode::Truncate { .. })
4630                        // `should_truncate` will be true for successful truncate checkpoint
4631                        && res.should_truncate()
4632                    {
4633                        state.phase = CheckpointPhase::TruncateDbFile {
4634                            sync_mode,
4635                            clear_page_cache,
4636                            page1_invalidated: false,
4637                        };
4638                    } else if res.wal_checkpoint_backfilled == 0
4639                        || sync_mode == crate::SyncMode::Off
4640                    {
4641                        state.phase = CheckpointPhase::Finalize { clear_page_cache };
4642                    } else {
4643                        state.phase = CheckpointPhase::SyncDbFile { clear_page_cache };
4644                    }
4645                    state.result = Some(res);
4646                }
4647                CheckpointPhase::TruncateDbFile {
4648                    sync_mode,
4649                    clear_page_cache,
4650                    page1_invalidated,
4651                } => {
4652                    let should_skip_truncate_db_file = {
4653                        let state = self.checkpoint_state.read();
4654                        turso_assert!(
4655                            matches!(state.mode, Some(CheckpointMode::Truncate { .. })),
4656                            "mode should be truncate in CheckpointPhase::TruncateDbFile"
4657                        );
4658                        let result = state.result.as_ref().expect("result should be set");
4659                        // Skip if we already sent truncate
4660                        result.db_truncate_sent
4661                    };
4662
4663                    if should_skip_truncate_db_file {
4664                        let mut state = self.checkpoint_state.write();
4665                        if sync_mode == crate::SyncMode::Off {
4666                            // Skip DB sync, proceed to WAL truncation
4667                            state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache };
4668                        } else {
4669                            // Sync DB first, then SyncDbFile will transition to TruncateWalFile
4670                            state.phase = CheckpointPhase::SyncDbFile { clear_page_cache };
4671                        }
4672                        continue;
4673                    }
4674                    // Invalidate page 1 (header) in cache before reading - checkpoint potentially wrote pages
4675                    // directly to DB file from the WAL, so the checkpointer connections' page 1 may have stale database_size.
4676                    if !page1_invalidated {
4677                        let page1_key = PageCacheKey::new(DatabaseHeader::PAGE_ID);
4678                        self.page_cache.write().delete(page1_key)?;
4679                        let mut state = self.checkpoint_state.write();
4680                        state.phase = CheckpointPhase::TruncateDbFile {
4681                            sync_mode,
4682                            clear_page_cache,
4683                            page1_invalidated: true,
4684                        };
4685                    }
4686
4687                    // Truncate the database file unless already at correct size
4688                    let db_size =
4689                        return_if_io!(self.with_header(|header| header.database_size)).get();
4690                    let page_size = self.get_page_size().unwrap_or_default();
4691                    let expected = db_size as u64 * page_size.get() as u64;
4692                    let should_skip_db_truncate = match self.db_file.size() {
4693                        Ok(current_size) => expected >= current_size,
4694                        Err(err) => {
4695                            // e.g. file.size() is not supported in web worker environment, so we should
4696                            // skip the truncate if we can't check the size.
4697                            tracing::debug!(
4698                                "checkpoint(TRUNCATE): db_file.size unavailable, skipping db truncate pre-check: {err}"
4699                            );
4700                            true
4701                        }
4702                    };
4703                    if should_skip_db_truncate {
4704                        // No DB truncation needed (or unsupported size pre-check), move to next phase.
4705                        let mut state = self.checkpoint_state.write();
4706                        if sync_mode == crate::SyncMode::Off {
4707                            // Skip DB sync, proceed to WAL truncation
4708                            state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache };
4709                        } else {
4710                            // Sync DB first, then SyncDbFile will transition to TruncateWalFile
4711                            state.phase = CheckpointPhase::SyncDbFile { clear_page_cache };
4712                        }
4713                        continue;
4714                    }
4715                    let c = self.db_file.truncate(
4716                        expected as usize,
4717                        Completion::new_trunc(move |_| {
4718                            tracing::trace!(
4719                                "Database file truncated to expected size: {} bytes",
4720                                expected
4721                            );
4722                        }),
4723                    )?;
4724                    self.checkpoint_state
4725                        .write()
4726                        .result
4727                        .as_mut()
4728                        .expect("result should be set")
4729                        .db_truncate_sent = true;
4730                    io_yield_one!(c);
4731                }
4732                CheckpointPhase::SyncDbFile { clear_page_cache } => {
4733                    let need_sync_db_file = {
4734                        let state = self.checkpoint_state.read();
4735                        let result = state.result.as_ref().expect("result should be set");
4736                        !result.db_sync_sent
4737                    };
4738
4739                    if !need_sync_db_file {
4740                        turso_assert!(
4741                            !self.syncing.load(Ordering::SeqCst),
4742                            "syncing should be done"
4743                        );
4744                        self.checkpoint_state.write().phase =
4745                            self.next_post_sync_checkpoint_phase(clear_page_cache);
4746                        continue;
4747                    }
4748
4749                    let c = sqlite3_ondisk::begin_sync(
4750                        self.db_file.as_ref(),
4751                        self.syncing.clone(),
4752                        self.get_sync_type(),
4753                    )?;
4754                    self.checkpoint_state
4755                        .write()
4756                        .result
4757                        .as_mut()
4758                        .expect("result should be set")
4759                        .db_sync_sent = true;
4760                    io_yield_one!(c);
4761                }
4762                CheckpointPhase::ReadDbIdentity {
4763                    clear_page_cache,
4764                    mut read,
4765                } => {
4766                    if !read.read_sent {
4767                        let header_buf = read.header_buf.clone();
4768                        let bytes_read = read.bytes_read.clone();
4769                        let c = self.db_file.read_header(Completion::new_read(header_buf, {
4770                            Box::new(move |res| {
4771                                if let Ok((_buf, count)) = res {
4772                                    bytes_read.store(count as usize, Ordering::Release);
4773                                }
4774                                None
4775                            })
4776                        }))?;
4777                        read.read_sent = true;
4778                        self.checkpoint_state.write().phase = CheckpointPhase::ReadDbIdentity {
4779                            clear_page_cache,
4780                            read,
4781                        };
4782                        io_yield_one!(c);
4783                    }
4784
4785                    let bytes_read = read.bytes_read.load(Ordering::Acquire);
4786                    if bytes_read < DatabaseHeader::SIZE {
4787                        return Err(LimboError::Corrupt(
4788                            "database header unreadable after checkpoint sync".into(),
4789                        ));
4790                    }
4791                    let (db_size_pages, db_header_crc32c) =
4792                        super::wal::database_identity_from_header_bytes(
4793                            &read.header_buf.as_slice()[..DatabaseHeader::SIZE],
4794                        )?;
4795                    if let Some(c) = wal.install_durable_backfill_proof(
4796                        read.max_frame,
4797                        db_size_pages,
4798                        db_header_crc32c,
4799                        self.get_sync_type(),
4800                    )? {
4801                        self.checkpoint_state.write().phase = CheckpointPhase::SyncBackfillProof {
4802                            clear_page_cache,
4803                            max_frame: read.max_frame,
4804                        };
4805                        io_yield_one!(c);
4806                    }
4807                    self.checkpoint_state.write().phase = CheckpointPhase::PublishBackfill {
4808                        clear_page_cache,
4809                        max_frame: read.max_frame,
4810                    };
4811                    continue;
4812                }
4813                CheckpointPhase::SyncBackfillProof {
4814                    clear_page_cache,
4815                    max_frame,
4816                } => {
4817                    self.checkpoint_state.write().phase = CheckpointPhase::PublishBackfill {
4818                        clear_page_cache,
4819                        max_frame,
4820                    };
4821                    continue;
4822                }
4823                CheckpointPhase::PublishBackfill {
4824                    clear_page_cache,
4825                    max_frame,
4826                } => {
4827                    {
4828                        let state = self.checkpoint_state.read();
4829                        let result = state.result.as_ref().expect("result should be set");
4830                        turso_assert!(
4831                            result.wal_checkpoint_backfilled > 0,
4832                            "PublishBackfill phase requires frames backfilled during checkpoint",
4833                            {
4834                                "publish_backfill": max_frame,
4835                                "wal_max_frame": result.wal_max_frame,
4836                                "wal_total_backfilled": result.wal_total_backfilled,
4837                                "wal_checkpoint_backfilled": result.wal_checkpoint_backfilled
4838                            }
4839                        );
4840                        turso_assert!(
4841                            max_frame == result.wal_total_backfilled,
4842                            "PublishBackfill target must match checkpoint result",
4843                            {
4844                                "publish_backfill": max_frame,
4845                                "wal_total_backfilled": result.wal_total_backfilled
4846                            }
4847                        );
4848                        turso_assert!(
4849                            result.wal_total_backfilled <= result.wal_max_frame,
4850                            "checkpoint result cannot backfill beyond WAL max frame",
4851                            {
4852                                "wal_total_backfilled": result.wal_total_backfilled,
4853                                "wal_max_frame": result.wal_max_frame
4854                            }
4855                        );
4856                    }
4857                    wal.publish_backfill(max_frame);
4858                    let next_phase = {
4859                        let state = self.checkpoint_state.read();
4860                        if matches!(state.mode, Some(CheckpointMode::Truncate { .. })) {
4861                            CheckpointPhase::TruncateWalFile { clear_page_cache }
4862                        } else {
4863                            CheckpointPhase::Finalize { clear_page_cache }
4864                        }
4865                    };
4866                    self.checkpoint_state.write().phase = next_phase;
4867                    continue;
4868                }
4869                CheckpointPhase::TruncateWalFile { clear_page_cache } => {
4870                    // Truncate WAL file after DB is safely synced - this ensures data durability.
4871                    // If crash occurred after WAL truncate but before DB sync, data would be lost.
4872                    let need_wal_truncate = {
4873                        let state = self.checkpoint_state.read();
4874                        turso_assert!(
4875                            matches!(state.mode, Some(CheckpointMode::Truncate { .. })),
4876                            "mode should be truncate in CheckpointPhase::TruncateWalFile"
4877                        );
4878                        let result = state.result.as_ref().expect("result should be set");
4879                        !result.wal_truncate_sent || !result.wal_sync_sent
4880                    };
4881
4882                    if !need_wal_truncate {
4883                        self.checkpoint_state.write().phase =
4884                            CheckpointPhase::Finalize { clear_page_cache };
4885                        continue;
4886                    }
4887
4888                    // Call WAL truncate
4889                    return_if_io!(wal.truncate_wal(
4890                        self.checkpoint_state
4891                            .write()
4892                            .result
4893                            .as_mut()
4894                            .expect("result should be set"),
4895                        self.get_sync_type(),
4896                    ));
4897                }
4898                CheckpointPhase::Finalize { clear_page_cache } => {
4899                    let mut state = self.checkpoint_state.write();
4900                    let mut res = state.result.take().expect("result should be set");
4901                    state.phase = CheckpointPhase::NotCheckpointing;
4902                    state.mode = None;
4903                    state.lock_source = CheckpointLockSource::Acquire;
4904
4905                    // Clear page cache only if requested (explicit checkpoints do this, auto-checkpoint does not)
4906                    if clear_page_cache {
4907                        self.invalidate_all_cursors();
4908                        self.page_cache.write().clear(false).map_err(|e| {
4909                            res.release_guard();
4910                            LimboError::InternalError(format!("Failed to clear page cache: {e:?}"))
4911                        })?;
4912                    }
4913
4914                    // Release checkpoint guard
4915                    res.release_guard();
4916
4917                    return Ok(IOResult::Done(res));
4918                }
4919            }
4920        }
4921    }
4922
4923    #[cfg(clt_turso_feature = "simulator")]
4924    pub fn run_checkpoint_until_post_sync_gap_for_testing(
4925        &self,
4926        mode: CheckpointMode,
4927    ) -> Result<u64> {
4928        loop {
4929            match self.checkpoint(mode, crate::SyncMode::Full, true)? {
4930                IOResult::Done(_) => {
4931                    return Err(LimboError::InternalError(
4932                        "checkpoint completed before reaching the post-sync pre-publish gap"
4933                            .to_string(),
4934                    ));
4935                }
4936                IOResult::IO(io) => io.wait(self.io.as_ref())?,
4937            }
4938
4939            let state = self.checkpoint_state.read();
4940            let Some(result) = state.result.as_ref() else {
4941                continue;
4942            };
4943            if matches!(state.phase, CheckpointPhase::ReadDbIdentity { .. })
4944                && result.db_sync_sent
4945                && !self.syncing.load(Ordering::SeqCst)
4946            {
4947                return Ok(result.wal_total_backfilled);
4948            }
4949        }
4950    }
4951
4952    /// Invalidates entire page cache by removing all dirty and clean pages. Usually used in case
4953    /// of a rollback or in case we want to invalidate page cache after starting a read transaction
4954    /// right after new writes happened which would invalidate current page cache.
4955    pub fn clear_page_cache(&self, clear_dirty: bool) {
4956        self.invalidate_all_cursors();
4957        let dirty_pages = self.dirty_pages.write();
4958        let mut cache = self.page_cache.write();
4959        for page_id in dirty_pages.iter() {
4960            let page_key = PageCacheKey::new(page_id as usize);
4961            if let Some(page) = cache.get(&page_key).unwrap_or(None) {
4962                page.clear_dirty();
4963            }
4964        }
4965        cache
4966            .clear(clear_dirty)
4967            .expect("Failed to clear page cache");
4968        if clear_dirty {
4969            drop(dirty_pages);
4970            self.dirty_pages.write().clear();
4971        }
4972    }
4973
4974    /// Checkpoint in Truncate mode and delete the WAL file. This method is _only_ to be called
4975    /// for shutting down the last remaining connection to a database.
4976    ///
4977    /// sqlite3.h
4978    /// Usually, when a database in [WAL mode] is closed or detached from a
4979    /// database handle, SQLite checks if if there are other connections to the
4980    /// same database, and if there are no other database connection (if the
4981    /// connection being closed is the last open connection to the database),
4982    /// then SQLite performs a [checkpoint] before closing the connection and
4983    /// deletes the WAL file.
4984    pub fn checkpoint_shutdown(
4985        &self,
4986        allowed_auto_actions: WalAutoActions,
4987        sync_mode: crate::SyncMode,
4988    ) -> Result<()> {
4989        let mut attempts = 0;
4990        {
4991            let Some(wal) = self.wal.as_ref() else {
4992                turso_soft_unreachable!("checkpoint_shutdown() called on database without WAL");
4993                return Err(LimboError::InternalError(
4994                    "checkpoint_shutdown() called on database without WAL".to_string(),
4995                ));
4996            };
4997            // fsync the wal syncronously before beginning checkpoint
4998            let c = wal.sync(self.get_sync_type())?;
4999            self.io.wait_for_completion(c)?;
5000        }
5001        if allowed_auto_actions.contains(WalAutoActions::Checkpoint) {
5002            while let Err(LimboError::Busy) = self.blocking_checkpoint(
5003                CheckpointMode::Truncate {
5004                    upper_bound_inclusive: None,
5005                },
5006                sync_mode,
5007            ) {
5008                if attempts == 3 {
5009                    // don't return error on `close` if we are unable to checkpoint, we can silently fail
5010                    tracing::warn!(
5011                        "Failed to checkpoint WAL on shutdown after 3 attempts, giving up"
5012                    );
5013                    return Ok(());
5014                }
5015                attempts += 1;
5016            }
5017        }
5018        // TODO: delete the WAL file here after truncate checkpoint, but *only* if we are sure that
5019        // no other connections have opened since.
5020        Ok(())
5021    }
5022
5023    /// Perform a blocking checkpoint with the specified mode.
5024    /// This is a convenience wrapper around `checkpoint()` that blocks until completion.
5025    /// Explicit checkpoints clear the page cache after completion.
5026    #[instrument(skip_all, level = Level::DEBUG)]
5027    pub fn blocking_checkpoint(
5028        &self,
5029        mode: CheckpointMode,
5030        sync_mode: crate::SyncMode,
5031    ) -> Result<CheckpointResult> {
5032        self.io.block(|| self.checkpoint(mode, sync_mode, true))
5033    }
5034
5035    pub fn freepage_list(&self) -> u32 {
5036        self.io
5037            .block(|| HeaderRef::from_pager(self))
5038            .map(|header_ref| header_ref.borrow().freelist_pages.get())
5039            .unwrap_or(0)
5040    }
5041    // Providing a page is optional, if provided it will be used to avoid reading the page from disk.
5042    // This is implemented in accordance with sqlite freepage2() function.
5043    #[instrument(skip_all, level = Level::DEBUG)]
5044    pub fn free_page(&self, mut page: Option<PageRef>, page_id: usize) -> Result<IOResult<()>> {
5045        tracing::trace!("free_page(page_id={})", page_id);
5046        // Number of reserved slots in trunk header (next pointer + leaf count)
5047        const RESERVED_SLOTS: usize = 2;
5048
5049        let header_ref = return_if_io!(HeaderRefMut::from_pager(self));
5050        let header = header_ref.borrow_mut();
5051
5052        let mut state = self.free_page_state.write();
5053        tracing::debug!(?state);
5054        loop {
5055            match &mut *state {
5056                FreePageState::Start => {
5057                    if page_id < 2 || page_id > header.database_size.get() as usize {
5058                        return Err(LimboError::Corrupt(format!(
5059                            "Invalid page number {page_id} for free operation"
5060                        )));
5061                    }
5062
5063                    // The first yield point is the `HeaderRefMut::from_pager`
5064                    // acquisition above the loop, not this read fork: if it
5065                    // yields for the page-1 read, re-entry re-runs that prefix
5066                    // (it is idempotent — the pager cache returns the same
5067                    // header page) before reaching `Start` again, where `state`
5068                    // is still `Start`. The read fork below is likewise safe:
5069                    // if the caller passes `Some(page)`, no IO occurs and the
5070                    // mutations below run synchronously. If the caller passes
5071                    // `None` and `read_page` yields for spill, we leave `state`
5072                    // at `Start` so re-entry re-takes either branch (the
5073                    // pager's `pending_reads` memoization returns the same
5074                    // `PageRef` the next time). Crucially, the non-idempotent
5075                    // mutations (`freelist_pages` increment, `page.pin()`,
5076                    // state advance) all happen AFTER both branches converge.
5077                    let (page, c) = match page.take() {
5078                        Some(page) => {
5079                            turso_assert_eq!(
5080                                page.get().id,
5081                                page_id,
5082                                "free_page page id mismatch",
5083                                { "expected": page_id, "actual": page.get().id }
5084                            );
5085                            (page, None)
5086                        }
5087                        None => return_if_io!(self.read_page(page_id as i64)),
5088                    };
5089                    page.get().overflow_cells.clear();
5090                    header.freelist_pages = (header.freelist_pages.get() + 1).into();
5091
5092                    let trunk_page_id = header.freelist_trunk_page.get();
5093
5094                    // Pin page to prevent eviction while stored in state machine
5095                    page.pin();
5096
5097                    if trunk_page_id != 0 {
5098                        *state = FreePageState::AddToTrunk { page };
5099                    } else {
5100                        *state = FreePageState::NewTrunk { page };
5101                    }
5102                    if let Some(c) = c {
5103                        if !c.succeeded() {
5104                            io_yield_one!(c);
5105                        }
5106                    }
5107                }
5108                FreePageState::AddToTrunk { page } => {
5109                    let trunk_page_id = header.freelist_trunk_page.get();
5110                    // Spill yield here keeps `state` at `AddToTrunk`. The
5111                    // subsequent writes / `unpin()` only run after we have
5112                    // a loaded `trunk_page`; on re-entry the pager's
5113                    // `pending_reads` returns the same `trunk_page`, and the
5114                    // writes are byte-identical (we haven't written yet so
5115                    // `number_of_leaf_pages` is unchanged).
5116                    let (trunk_page, c) = return_if_io!(self.read_page(trunk_page_id as i64));
5117                    if let Some(c) = c {
5118                        if !c.succeeded() {
5119                            io_yield_one!(c);
5120                        }
5121                    }
5122                    turso_assert!(trunk_page.is_loaded(), "trunk_page should be loaded");
5123
5124                    let trunk_page_contents = trunk_page.get_contents();
5125                    let number_of_leaf_pages =
5126                        trunk_page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT);
5127
5128                    let max_free_list_entries =
5129                        (header.usable_space() / FREELIST_LEAF_PTR_SIZE) - RESERVED_SLOTS;
5130
5131                    if number_of_leaf_pages < max_free_list_entries as u32 {
5132                        turso_assert!(
5133                            trunk_page.get().id == trunk_page_id as usize,
5134                            "trunk page has unexpected id"
5135                        );
5136                        self.add_dirty(&trunk_page)?;
5137
5138                        trunk_page_contents.write_u32_no_offset(
5139                            FREELIST_TRUNK_OFFSET_LEAF_COUNT,
5140                            number_of_leaf_pages + 1,
5141                        );
5142                        trunk_page_contents.write_u32_no_offset(
5143                            FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR
5144                                + (number_of_leaf_pages as usize * FREELIST_LEAF_PTR_SIZE),
5145                            page_id as u32,
5146                        );
5147
5148                        // Unpin page before finishing - it's added to freelist
5149                        page.unpin();
5150                        break;
5151                    }
5152                    // page remains pinned as it transitions to NewTrunk state
5153                    *state = FreePageState::NewTrunk { page: page.clone() };
5154                }
5155                FreePageState::NewTrunk { page } => {
5156                    turso_assert!(page.is_loaded(), "page should be loaded");
5157                    // If we get here, need to make this page a new trunk
5158                    turso_assert!(page.get().id == page_id, "page has unexpected id");
5159                    self.add_dirty(page)?;
5160
5161                    let trunk_page_id = header.freelist_trunk_page.get();
5162
5163                    let contents = page.get_contents();
5164                    // Point to previous trunk
5165                    contents
5166                        .write_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR, trunk_page_id);
5167                    // Zero leaf count
5168                    contents.write_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT, 0);
5169                    // Update page 1 to point to new trunk
5170                    header.freelist_trunk_page = (page_id as u32).into();
5171                    // Unpin page before finishing - it's now a trunk page
5172                    page.unpin();
5173                    break;
5174                }
5175            }
5176        }
5177        *state = FreePageState::Start;
5178        Ok(IOResult::Done(()))
5179    }
5180
5181    #[instrument(skip_all, level = Level::DEBUG)]
5182    pub fn allocate_page1(&self) -> Result<IOResult<PageRef>> {
5183        let state = self.allocate_page1_state.read().clone();
5184        match state {
5185            AllocatePage1State::Start => {
5186                turso_assert!(!self.db_initialized());
5187                tracing::trace!("allocate_page1(Start)");
5188
5189                let IOResult::Done(mut default_header) = self.with_header(|header| *header)? else {
5190                    panic!("DB should not be initialized and should not do any IO");
5191                };
5192
5193                turso_assert_eq!(default_header.database_size.get(), 0);
5194                default_header.database_size = 1.into();
5195
5196                // Use cached reserved_space if set (e.g., by sync engine before page allocation),
5197                // otherwise fall back to IOContext's encryption/checksum requirements.
5198                let reserved_space_bytes = self.get_reserved_space().unwrap_or_else(|| {
5199                    let io_ctx = self.io_ctx.read();
5200                    io_ctx.get_reserved_space_bytes()
5201                });
5202                default_header.reserved_space = reserved_space_bytes;
5203                self.set_reserved_space(reserved_space_bytes);
5204
5205                if let Some(size) = self.get_page_size() {
5206                    default_header.page_size = size;
5207                }
5208
5209                tracing::debug!(
5210                    "allocate_page1(Start) page_size = {:?}, reserved_space = {}",
5211                    default_header.page_size,
5212                    default_header.reserved_space
5213                );
5214
5215                self.buffer_pool
5216                    .finalize_with_page_size(default_header.page_size.get() as usize)?;
5217                let page = allocate_new_page(1, &self.buffer_pool);
5218
5219                let contents = page.get_contents();
5220                contents.write_database_header(&default_header);
5221
5222                let page1 = page;
5223                // Create the sqlite_schema table, for this we just need to create the btree page
5224                // for the first page of the database which is basically like any other btree page
5225                // but with a 100 byte offset, so we just init the page so that sqlite understands
5226                // this is a correct page.
5227                btree_init_page(
5228                    &page1,
5229                    PageType::TableLeaf,
5230                    DatabaseHeader::SIZE,
5231                    (default_header.page_size.get() - default_header.reserved_space as u32)
5232                        as usize,
5233                );
5234                let c = begin_write_btree_page(self, &page1)?;
5235
5236                // Pin page1 to prevent eviction while stored in state machine
5237                page1.pin();
5238                *self.allocate_page1_state.write() = AllocatePage1State::Writing { page: page1 };
5239                io_yield_one!(c);
5240            }
5241            AllocatePage1State::Writing { page } => {
5242                turso_assert!(page.is_loaded(), "page should be loaded");
5243                tracing::trace!("allocate_page1(Writing done)");
5244                let page_key = PageCacheKey::new(page.get().id);
5245                let mut cache = self.page_cache.write();
5246                cache.insert(page_key, page.clone()).map_err(|e| {
5247                    LimboError::InternalError(format!("Failed to insert page 1 into cache: {e:?}"))
5248                })?;
5249                // After we wrote the header page, we may now set this None, to signify we initialized
5250                self.init_page_1.store(None);
5251                page.unpin();
5252                *self.allocate_page1_state.write() = AllocatePage1State::Done;
5253                Ok(IOResult::Done(page))
5254            }
5255            AllocatePage1State::Done => unreachable!("cannot try to allocate page 1 again"),
5256        }
5257    }
5258
5259    pub fn allocating_page1(&self) -> bool {
5260        matches!(
5261            *self.allocate_page1_state.read(),
5262            AllocatePage1State::Writing { .. }
5263        )
5264    }
5265
5266    /// Tries to reuse a page from the freelist if available.
5267    /// If not, allocates a new page which increases the database size.
5268    ///
5269    /// FIXME: implement sqlite's 'nearby' parameter and use AllocMode.
5270    ///        SQLite's allocate_page() equivalent has a parameter 'nearby' which is a hint about the page number we want to have for the allocated page.
5271    ///        We should use this parameter to allocate the page in the same way as SQLite does; instead now we just either take the first available freelist page
5272    ///        or allocate a new page.
5273    #[allow(clippy::readonly_write_lock)]
5274    #[instrument(skip_all, level = Level::DEBUG)]
5275    pub fn allocate_page(&self) -> Result<IOResult<PageRef>> {
5276        // Ensure cache has room before allocating (we may spill dirty pages first)
5277        return_if_io!(self.ensure_cache_space());
5278
5279        let header_ref = return_if_io!(HeaderRefMut::from_pager(self));
5280        let header = header_ref.borrow_mut();
5281
5282        loop {
5283            let mut state = self.allocate_page_state.write();
5284            tracing::debug!("allocate_page(state={:?})", state);
5285            match &mut *state {
5286                AllocatePageState::Start => {
5287                    let old_db_size = header.database_size.get();
5288                    #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
5289                    let mut new_db_size = old_db_size;
5290                    #[cfg(clt_turso_feature = "omit_autovacuum")]
5291                    let new_db_size = old_db_size;
5292
5293                    tracing::debug!("allocate_page(database_size={})", new_db_size);
5294                    #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
5295                    {
5296                        //  If the following conditions are met, allocate a pointer map page, add to cache and increment the database size
5297                        //  - autovacuum is enabled
5298                        //  - the last page is a pointer map page
5299                        if matches!(
5300                            AutoVacuumMode::from(self.auto_vacuum_mode.load(Ordering::SeqCst)),
5301                            AutoVacuumMode::Full
5302                        ) && is_ptrmap_page(new_db_size + 1, header.page_size.get() as usize)
5303                        {
5304                            // we will allocate a ptrmap page, so increment size
5305                            new_db_size += 1;
5306                            // Make the ptrmap allocation idempotent across
5307                            // spill-yield re-entries: only allocate + insert
5308                            // if the cache doesn't already contain it. The
5309                            // read-then-write pattern is safe because
5310                            // `allocate_page` holds the only writer for
5311                            // `database_size`/`freelist_trunk_page`; no
5312                            // concurrent caller can race in between.
5313                            let page_key = PageCacheKey::new(new_db_size as usize);
5314                            let already_present = {
5315                                let cache = self.page_cache.read();
5316                                cache.contains_key(&page_key)
5317                            };
5318                            if !already_present {
5319                                let page = allocate_new_page(new_db_size as i64, &self.buffer_pool);
5320                                self.add_dirty(&page)?;
5321                                self.page_cache.write().force_insert_page(page_key, page)?;
5322                            }
5323                        }
5324                    }
5325
5326                    let first_freelist_trunk_page_id = header.freelist_trunk_page.get();
5327                    if first_freelist_trunk_page_id == 0 {
5328                        *state = AllocatePageState::AllocateNewPage {
5329                            current_db_size: new_db_size,
5330                        };
5331                        continue;
5332                    }
5333                    // Spill yield routes back through `Start`; the ptrmap
5334                    // allocation above is idempotent and `trunk_page.pin()`
5335                    // happens only after `Done`, so no double-pin.
5336                    let (trunk_page, c) =
5337                        return_if_io!(self.read_page(first_freelist_trunk_page_id as i64));
5338                    trunk_page.pin();
5339                    *state = AllocatePageState::SearchAvailableFreeListLeaf { trunk_page };
5340                    if let Some(c) = c {
5341                        io_yield_one!(c);
5342                    }
5343                }
5344                AllocatePageState::SearchAvailableFreeListLeaf { trunk_page } => {
5345                    turso_assert!(
5346                        trunk_page.is_loaded(),
5347                        "Freelist trunk page is not loaded",
5348                        { "page_id": trunk_page.get().id }
5349                    );
5350                    let page_contents = trunk_page.get_contents();
5351                    let next_trunk_page_id =
5352                        page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR);
5353                    let number_of_freelist_leaves =
5354                        page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT);
5355
5356                    // There are leaf pointers on this trunk page, so we can reuse one of the pages
5357                    // for the allocation.
5358                    if number_of_freelist_leaves != 0 {
5359                        let page_contents = trunk_page.get_contents();
5360                        let next_leaf_page_id =
5361                            page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR);
5362                        // Pin + state-advance happen only on `Done` so a
5363                        // spill yield doesn't double-pin the leaf page.
5364                        let (leaf_page, c) =
5365                            return_if_io!(self.read_page(next_leaf_page_id as i64));
5366                        turso_assert!(
5367                            number_of_freelist_leaves > 0,
5368                            "Freelist trunk page has no leaves",
5369                            { "page_id": trunk_page.get().id }
5370                        );
5371
5372                        // Pin leaf_page to prevent eviction while stored in state machine
5373                        // trunk_page is already pinned from previous state
5374                        leaf_page.pin();
5375
5376                        *state = AllocatePageState::ReuseFreelistLeaf {
5377                            trunk_page: trunk_page.clone(),
5378                            leaf_page,
5379                            number_of_freelist_leaves,
5380                        };
5381                        if let Some(c) = c {
5382                            io_yield_one!(c);
5383                        }
5384                        continue;
5385                    }
5386
5387                    // No freelist leaves on this trunk page.
5388                    // Reuse the trunk page itself (even if this is the last trunk).
5389                    // Update the database's first freelist trunk page to the next trunk page (may be 0 if there are no more trunk pages).
5390                    header.freelist_trunk_page = next_trunk_page_id.into();
5391                    header.freelist_pages = (header.freelist_pages.get() - 1).into();
5392                    self.add_dirty(trunk_page)?;
5393                    // zero out the page
5394                    turso_assert!(
5395                        trunk_page.get_contents().overflow_cells.is_empty(),
5396                        "Freelist trunk page has overflow cells",
5397                        { "page_id": trunk_page.get().id }
5398                    );
5399                    trunk_page.get_contents().as_ptr().fill(0);
5400                    let page_key = PageCacheKey::new(trunk_page.get().id);
5401                    {
5402                        let page_cache = self.page_cache.read();
5403                        turso_assert!(
5404                            page_cache.contains_key(&page_key),
5405                            "page is not in cache",
5406                            { "page_id": trunk_page.get().id }
5407                        );
5408                    }
5409                    // Unpin trunk_page before returning - caller takes ownership
5410                    trunk_page.unpin();
5411                    let trunk_page = trunk_page.clone();
5412                    *state = AllocatePageState::Start;
5413                    return Ok(IOResult::Done(trunk_page));
5414                }
5415                AllocatePageState::ReuseFreelistLeaf {
5416                    trunk_page,
5417                    leaf_page,
5418                    number_of_freelist_leaves,
5419                } => {
5420                    turso_assert!(
5421                        leaf_page.is_loaded(),
5422                        "Leaf page is not loaded",
5423                        { "page_id": leaf_page.get().id }
5424                    );
5425                    let page_contents = trunk_page.get_contents();
5426                    self.add_dirty(leaf_page)?;
5427                    // zero out the page
5428                    turso_assert!(
5429                        leaf_page.get_contents().overflow_cells.is_empty(),
5430                        "Freelist leaf page has overflow cells",
5431                        { "page_id": leaf_page.get().id }
5432                    );
5433                    leaf_page.get_contents().as_ptr().fill(0);
5434                    let page_key = PageCacheKey::new(leaf_page.get().id);
5435                    {
5436                        let page_cache = self.page_cache.read();
5437                        turso_assert!(
5438                            page_cache.contains_key(&page_key),
5439                            "page is not in cache",
5440                            { "page_id": leaf_page.get().id }
5441                        );
5442                    }
5443
5444                    // Mark trunk page dirty BEFORE modifying it so subjournal captures original content
5445                    self.add_dirty(trunk_page)?;
5446
5447                    // Shift left all the other leaf pages in the trunk page and subtract 1 from the leaf count
5448                    let remaining_leaves_count = (*number_of_freelist_leaves - 1) as usize;
5449                    {
5450                        let buf = page_contents.as_ptr();
5451                        // use copy within the same page
5452                        let offset_remaining_leaves_start =
5453                            FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR + FREELIST_LEAF_PTR_SIZE;
5454                        let offset_remaining_leaves_end = offset_remaining_leaves_start
5455                            + remaining_leaves_count * FREELIST_LEAF_PTR_SIZE;
5456                        buf.copy_within(
5457                            offset_remaining_leaves_start..offset_remaining_leaves_end,
5458                            FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR,
5459                        );
5460                    }
5461                    // write the new leaf count
5462                    page_contents.write_u32_no_offset(
5463                        FREELIST_TRUNK_OFFSET_LEAF_COUNT,
5464                        remaining_leaves_count as u32,
5465                    );
5466
5467                    header.freelist_pages = (header.freelist_pages.get() - 1).into();
5468                    // Unpin both pages before returning - caller takes ownership of leaf_page
5469                    trunk_page.unpin();
5470                    leaf_page.unpin();
5471                    let leaf_page = leaf_page.clone();
5472                    *state = AllocatePageState::Start;
5473                    return Ok(IOResult::Done(leaf_page));
5474                }
5475                AllocatePageState::AllocateNewPage { current_db_size } => {
5476                    let mut new_db_size = *current_db_size + 1;
5477
5478                    // if new_db_size reaches the pending page, we need to allocate a new one
5479                    if Some(new_db_size) == self.pending_byte_page_id() {
5480                        let richard_hipp_special_page =
5481                            allocate_new_page(new_db_size as i64, &self.buffer_pool);
5482                        self.add_dirty(&richard_hipp_special_page)?;
5483                        let page_key = PageCacheKey::new(richard_hipp_special_page.get().id);
5484                        self.page_cache
5485                            .write()
5486                            .force_insert_page(page_key, richard_hipp_special_page)?;
5487                        // HIPP special page is assumed to zeroed and should never be read or written to by the BTREE
5488                        new_db_size += 1;
5489                    }
5490
5491                    // Check if allocating a new page would exceed the maximum page count
5492                    let max_page_count = self.get_max_page_count();
5493                    if new_db_size > max_page_count {
5494                        return Err(LimboError::DatabaseFull(
5495                            "database or disk is full".to_string(),
5496                        ));
5497                    }
5498
5499                    // FIXME: should reserve page cache entry before modifying the database
5500                    let page = allocate_new_page(new_db_size as i64, &self.buffer_pool);
5501                    {
5502                        // setup page and add to cache
5503                        self.add_dirty(&page)?;
5504
5505                        let page_key = PageCacheKey::new(page.get().id as usize);
5506                        self.page_cache
5507                            .write()
5508                            .force_insert_page(page_key, page.clone())?;
5509                        header.database_size = new_db_size.into();
5510                        *state = AllocatePageState::Start;
5511                        return Ok(IOResult::Done(page));
5512                    }
5513                }
5514            }
5515        }
5516    }
5517
5518    pub fn upsert_page_in_cache(
5519        &self,
5520        id: usize,
5521        page: PageRef,
5522        dirty_page_must_exist: bool,
5523    ) -> Result<(), LimboError> {
5524        let mut cache = self.page_cache.write();
5525        let page_key = PageCacheKey::new(id);
5526
5527        // FIXME: use specific page key for writer instead of max frame, this will make readers not conflict
5528        if dirty_page_must_exist {
5529            turso_assert!(page.is_dirty(), "page must be dirty for upsert", { "page_id": id });
5530        }
5531        // The page carries writes that must stay cache-resident, so admit it
5532        // over capacity when nothing is evictable.
5533        cache
5534            .force_upsert_page(page_key, page.clone())
5535            .map_err(|e| {
5536                LimboError::InternalError(format!(
5537                    "Failed to insert loaded page {id} into cache: {e:?}"
5538                ))
5539            })?;
5540        page.set_loaded();
5541        page.clear_wal_tag();
5542        Ok(())
5543    }
5544
5545    fn force_upsert_page_in_cache(&self, id: usize, page: PageRef) -> Result<(), LimboError> {
5546        let mut cache = self.page_cache.write();
5547        let page_key = PageCacheKey::new(id);
5548
5549        turso_assert!(
5550            page.is_dirty(),
5551            "restored savepoint page must be dirty",
5552            { "page_id": id }
5553        );
5554        cache
5555            .force_upsert_page(page_key, page.clone())
5556            .map_err(|e| {
5557                LimboError::InternalError(format!(
5558                    "Failed to restore savepoint page {id} into cache: {e:?}"
5559                ))
5560            })?;
5561        page.set_loaded();
5562        page.clear_wal_tag();
5563        Ok(())
5564    }
5565
5566    #[instrument(skip_all, level = Level::DEBUG)]
5567    pub fn rollback(&self, schema_did_change: bool, connection: &Connection, is_write: bool) {
5568        tracing::debug!(schema_did_change);
5569        if is_write {
5570            let clear_dirty = true;
5571            // The page cache only needs to be cleared if we are rolling back a write transaction.
5572            // If a read transaction rolls back, and the next read transaction detects that the
5573            // database has changed in between (see db_changed() in wal.rs), then the page cache
5574            // will be cleared. Since the read transaction itself has not modified anything, it can proceed
5575            // with its cached pages in case the database has NOT changed in between.
5576            //
5577            // Even in the case of a write transaction, clearing the entire page cache is overkill,
5578            // since we only need to clear the dirty pages that were modified by the write transaction.
5579            self.clear_page_cache(clear_dirty);
5580            self.dirty_pages.write().clear();
5581        } else {
5582            turso_assert!(
5583                self.dirty_pages.read().is_empty(),
5584                "dirty pages should be empty for read txn"
5585            );
5586        }
5587        self.reset_internal_states();
5588        // Invalidate cached schema cookie since rollback may have restored the database schema cookie
5589        self.set_schema_cookie(None);
5590        if schema_did_change {
5591            *connection.schema.write() = connection.db.clone_schema();
5592        }
5593        if is_write {
5594            if let Some(wal) = self.wal.as_ref() {
5595                wal.rollback(None);
5596            }
5597        }
5598    }
5599
5600    fn reset_internal_states(&self) {
5601        self.pending_reads.write().clear();
5602        *self.checkpoint_state.write() = CheckpointState::default();
5603        self.syncing.store(false, Ordering::SeqCst);
5604        self.commit_info.write().reset();
5605        *self.allocate_page_state.write() = AllocatePageState::Start;
5606        *self.free_page_state.write() = FreePageState::Start;
5607        *self.spill_state.write() = SpillState::Idle;
5608        #[cfg(not(clt_turso_feature = "omit_autovacuum"))]
5609        {
5610            let mut vacuum_state = self.vacuum_state.write();
5611            vacuum_state.ptrmap_get_state = PtrMapGetState::Start;
5612            vacuum_state.ptrmap_put_state = PtrMapPutState::Start;
5613            vacuum_state.btree_create_vacuum_full_state = BtreeCreateVacuumFullState::Start;
5614        }
5615
5616        *self.header_ref_state.write() = HeaderRefState::Start;
5617    }
5618
5619    pub fn with_header<T>(&self, f: impl Fn(&DatabaseHeader) -> T) -> Result<IOResult<T>> {
5620        let header_ref = return_if_io!(HeaderRef::from_pager(self));
5621        let header = header_ref.borrow();
5622        // Update cached schema cookie when reading header
5623        self.set_schema_cookie(Some(header.schema_cookie.get()));
5624        Ok(IOResult::Done(f(header)))
5625    }
5626
5627    pub fn with_header_mut<T>(&self, f: impl Fn(&mut DatabaseHeader) -> T) -> Result<IOResult<T>> {
5628        let header_ref = return_if_io!(HeaderRefMut::from_pager(self));
5629        let header = header_ref.borrow_mut();
5630        let result = f(header);
5631        // Update cached schema cookie after modification
5632        self.set_schema_cookie(Some(header.schema_cookie.get()));
5633        Ok(IOResult::Done(result))
5634    }
5635
5636    pub fn is_encryption_ctx_set(&self) -> bool {
5637        self.io_ctx.read().encryption_context().is_some()
5638    }
5639
5640    pub fn is_encryption_enabled(&self) -> bool {
5641        self.enable_encryption.load(Ordering::SeqCst)
5642    }
5643
5644    pub fn set_encryption_context(
5645        &self,
5646        cipher_mode: CipherMode,
5647        key: &EncryptionKey,
5648    ) -> Result<()> {
5649        // we will set the encryption context only if the encryption is opted-in.
5650        if !self.enable_encryption.load(Ordering::SeqCst) {
5651            return Err(LimboError::InvalidArgument(
5652                "encryption is an opt in feature. enable it via passing `--experimental-encryption`"
5653                    .into(),
5654            ));
5655        }
5656
5657        let page_size = self.get_page_size_unchecked().get() as usize;
5658        let encryption_ctx = EncryptionContext::new(cipher_mode, key, page_size)?;
5659        {
5660            let mut io_ctx = self.io_ctx.write();
5661            io_ctx.set_encryption(encryption_ctx);
5662        }
5663        let Some(wal) = self.wal.as_ref() else {
5664            return Ok(());
5665        };
5666        wal.set_io_context(self.io_ctx.read().clone());
5667        // whenever we set the encryption context, lets reset the page cache. The page cache
5668        // might have been loaded with page 1 to initialise the connection. During initialisation,
5669        // we only read the header which is unencrypted, but the rest of the page is. If so, lets
5670        // clear the cache.
5671        self.clear_page_cache(false);
5672        // Also invalidate cached schema cookie to force re-read of page 1 with encryption
5673        self.set_schema_cookie(None);
5674        Ok(())
5675    }
5676
5677    pub fn reset_checksum_context(&self) {
5678        {
5679            let mut io_ctx = self.io_ctx.write();
5680            io_ctx.reset_checksum();
5681        }
5682        let Some(wal) = self.wal.as_ref() else { return };
5683        wal.set_io_context(self.io_ctx.read().clone())
5684    }
5685
5686    pub fn set_reserved_space_bytes(&self, value: u8) {
5687        self.set_reserved_space(value);
5688    }
5689
5690    /// Encryption is an opt-in feature. If the flag is passed, then enable the encryption on
5691    /// pager, which is then used to set it on the IOContext.
5692    pub fn enable_encryption(&self, enable: bool) {
5693        self.enable_encryption.store(enable, Ordering::SeqCst);
5694    }
5695}
5696
5697pub fn allocate_new_page(page_id: i64, buffer_pool: &Arc<BufferPool>) -> PageRef {
5698    let page = Arc::new(Page::new(page_id));
5699    {
5700        let buffer = buffer_pool.get_page();
5701        let inner = page.get();
5702        inner.buffer = Some(Arc::new(buffer));
5703        page.set_loaded();
5704        page.clear_wal_tag();
5705    }
5706    page
5707}
5708
5709pub fn default_page1(cipher: Option<&CipherMode>) -> PageRef {
5710    // New Database header for empty Database
5711    let mut default_header = DatabaseHeader::default();
5712
5713    if let Some(cipher) = cipher {
5714        // we will set the reserved space bytes as required by either the encryption
5715        let reserved_space_bytes = cipher.metadata_size() as u8;
5716        default_header.reserved_space = reserved_space_bytes;
5717    }
5718
5719    let page = Arc::new(Page::new(DatabaseHeader::PAGE_ID as i64));
5720
5721    {
5722        let inner = page.get();
5723        inner.buffer = Some(Arc::new(Buffer::new_temporary(
5724            default_header.page_size.get() as usize,
5725        )));
5726    }
5727
5728    page.get_contents().write_database_header(&default_header);
5729    page.set_loaded();
5730    page.clear_wal_tag();
5731
5732    btree_init_page(
5733        &page,
5734        PageType::TableLeaf,
5735        DatabaseHeader::SIZE, // offset of 100 bytes
5736        (default_header.page_size.get() - default_header.reserved_space as u32) as usize,
5737    );
5738
5739    page
5740}
5741
5742#[derive(Debug, Clone, Copy)]
5743pub struct CreateBTreeFlags(pub u8);
5744impl CreateBTreeFlags {
5745    pub const TABLE: u8 = 0b0001;
5746    pub const INDEX: u8 = 0b0010;
5747
5748    pub fn new_table() -> Self {
5749        Self(CreateBTreeFlags::TABLE)
5750    }
5751
5752    pub fn new_index() -> Self {
5753        Self(CreateBTreeFlags::INDEX)
5754    }
5755
5756    pub fn is_table(&self) -> bool {
5757        (self.0 & CreateBTreeFlags::TABLE) != 0
5758    }
5759
5760    pub fn is_index(&self) -> bool {
5761        (self.0 & CreateBTreeFlags::INDEX) != 0
5762    }
5763
5764    pub fn get_flags(&self) -> u8 {
5765        self.0
5766    }
5767}
5768
5769/*
5770** The pointer map is a lookup table that identifies the parent page for
5771** each child page in the database file.  The parent page is the page that
5772** contains a pointer to the child.  Every page in the database contains
5773** 0 or 1 parent pages. Each pointer map entry consists of a single byte 'type'
5774** and a 4 byte parent page number.
5775**
5776** The PTRMAP_XXX identifiers below are the valid types.
5777**
5778** The purpose of the pointer map is to facilitate moving pages from one
5779** position in the file to another as part of autovacuum.  When a page
5780** is moved, the pointer in its parent must be updated to point to the
5781** new location.  The pointer map is used to locate the parent page quickly.
5782**
5783** PTRMAP_ROOTPAGE: The database page is a root-page. The page-number is not
5784**                  used in this case.
5785**
5786** PTRMAP_FREEPAGE: The database page is an unused (free) page. The page-number
5787**                  is not used in this case.
5788**
5789** PTRMAP_OVERFLOW1: The database page is the first page in a list of
5790**                   overflow pages. The page number identifies the page that
5791**                   contains the cell with a pointer to this overflow page.
5792**
5793** PTRMAP_OVERFLOW2: The database page is the second or later page in a list of
5794**                   overflow pages. The page-number identifies the previous
5795**                   page in the overflow page list.
5796**
5797** PTRMAP_BTREE: The database page is a non-root btree page. The page number
5798**               identifies the parent page in the btree.
5799*/
5800#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
5801pub(crate) mod ptrmap {
5802    #[allow(unused_imports)]
5803    use crate::{storage::sqlite3_ondisk::PageSize, LimboError, Result};
5804    use crate::{turso_assert_greater_than_or_equal, turso_soft_unreachable};
5805
5806    // Constants
5807    pub const PTRMAP_ENTRY_SIZE: usize = 5;
5808    /// Page 1 is the schema page which contains the database header.
5809    /// Page 2 is the first pointer map page if the database has any pointer map pages.
5810    pub const FIRST_PTRMAP_PAGE_NO: u32 = 2;
5811
5812    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5813    #[repr(u8)]
5814    pub enum PtrmapType {
5815        RootPage = 1,
5816        FreePage = 2,
5817        Overflow1 = 3,
5818        Overflow2 = 4,
5819        BTreeNode = 5,
5820    }
5821
5822    impl PtrmapType {
5823        pub fn from_u8(value: u8) -> Option<Self> {
5824            match value {
5825                1 => Some(PtrmapType::RootPage),
5826                2 => Some(PtrmapType::FreePage),
5827                3 => Some(PtrmapType::Overflow1),
5828                4 => Some(PtrmapType::Overflow2),
5829                5 => Some(PtrmapType::BTreeNode),
5830                _ => None,
5831            }
5832        }
5833    }
5834
5835    #[derive(Debug, Clone, Copy)]
5836    pub struct PtrmapEntry {
5837        pub entry_type: PtrmapType,
5838        pub parent_page_no: u32,
5839    }
5840
5841    impl PtrmapEntry {
5842        pub fn serialize(&self, buffer: &mut [u8]) -> Result<()> {
5843            if buffer.len() < PTRMAP_ENTRY_SIZE {
5844                return Err(LimboError::InternalError(format!(
5845                    "Buffer too small to serialize ptrmap entry. Expected at least {} bytes, got {}",
5846                    PTRMAP_ENTRY_SIZE,
5847                    buffer.len()
5848                )));
5849            }
5850            buffer[0] = self.entry_type as u8;
5851            buffer[1..5].copy_from_slice(&self.parent_page_no.to_be_bytes());
5852            Ok(())
5853        }
5854
5855        pub fn deserialize(buffer: &[u8]) -> Option<Self> {
5856            if buffer.len() < PTRMAP_ENTRY_SIZE {
5857                return None;
5858            }
5859            let entry_type_u8 = buffer[0];
5860            let parent_bytes_slice = buffer.get(1..5)?;
5861            let parent_page_no = u32::from_be_bytes(parent_bytes_slice.try_into().ok()?);
5862            PtrmapType::from_u8(entry_type_u8).map(|entry_type| PtrmapEntry {
5863                entry_type,
5864                parent_page_no,
5865            })
5866        }
5867    }
5868
5869    /// Calculates how many database pages are mapped by a single pointer map page.
5870    /// This is based on the total page size, as ptrmap pages are filled with entries.
5871    pub fn entries_per_ptrmap_page(page_size: usize) -> usize {
5872        turso_assert_greater_than_or_equal!(page_size, PageSize::MIN as usize);
5873        page_size / PTRMAP_ENTRY_SIZE
5874    }
5875
5876    /// Calculates the cycle length of pointer map pages
5877    /// The cycle length is the number of database pages that are mapped by a single pointer map page.
5878    pub fn ptrmap_page_cycle_length(page_size: usize) -> usize {
5879        turso_assert_greater_than_or_equal!(page_size, PageSize::MIN as usize);
5880        (page_size / PTRMAP_ENTRY_SIZE) + 1
5881    }
5882
5883    /// Determines if a given page number `db_page_no` (1-indexed) is a pointer map page in a database with autovacuum enabled
5884    pub fn is_ptrmap_page(db_page_no: u32, page_size: usize) -> bool {
5885        //  The first page cannot be a ptrmap page because its for the schema
5886        if db_page_no == 1 {
5887            return false;
5888        }
5889        if db_page_no == FIRST_PTRMAP_PAGE_NO {
5890            return true;
5891        }
5892        get_ptrmap_page_no_for_db_page(db_page_no, page_size) == db_page_no
5893    }
5894
5895    /// Calculates which pointer map page (1-indexed) contains the entry for `db_page_no_to_query` (1-indexed).
5896    /// `db_page_no_to_query` is the page whose ptrmap entry we are interested in.
5897    pub fn get_ptrmap_page_no_for_db_page(db_page_no_to_query: u32, page_size: usize) -> u32 {
5898        let group_size = ptrmap_page_cycle_length(page_size) as u32;
5899        if group_size == 0 {
5900            panic!("Page size too small, a ptrmap page cannot map any db pages.");
5901        }
5902
5903        let effective_page_index = db_page_no_to_query - FIRST_PTRMAP_PAGE_NO;
5904        let group_idx = effective_page_index / group_size;
5905
5906        (group_idx * group_size) + FIRST_PTRMAP_PAGE_NO
5907    }
5908
5909    /// Calculates the byte offset of the entry for `db_page_no_to_query` (1-indexed)
5910    /// within its pointer map page (`ptrmap_page_no`, 1-indexed).
5911    pub fn get_ptrmap_offset_in_page(
5912        db_page_no_to_query: u32,
5913        ptrmap_page_no: u32,
5914        page_size: usize,
5915    ) -> Result<usize> {
5916        // The data pages mapped by `ptrmap_page_no` are:
5917        // `ptrmap_page_no + 1`, `ptrmap_page_no + 2`, ..., up to `ptrmap_page_no + n_data_pages_per_group`.
5918        // `db_page_no_to_query` must be one of these.
5919        // The 0-indexed position of `db_page_no_to_query` within this sequence of data pages is:
5920        // `db_page_no_to_query - (ptrmap_page_no + 1)`.
5921
5922        let n_data_pages_per_group = entries_per_ptrmap_page(page_size);
5923        let first_data_page_mapped = ptrmap_page_no + 1;
5924        let last_data_page_mapped = ptrmap_page_no + n_data_pages_per_group as u32;
5925
5926        if db_page_no_to_query < first_data_page_mapped
5927            || db_page_no_to_query > last_data_page_mapped
5928        {
5929            turso_soft_unreachable!("Page is not mapped by ptrmap data range", { "page": db_page_no_to_query, "range_start": first_data_page_mapped, "range_end": last_data_page_mapped, "ptrmap_page": ptrmap_page_no });
5930            return Err(LimboError::InternalError(format!(
5931                "Page {db_page_no_to_query} is not mapped by the data page range [{first_data_page_mapped}, {last_data_page_mapped}] of ptrmap page {ptrmap_page_no}"
5932            )));
5933        }
5934        if is_ptrmap_page(db_page_no_to_query, page_size) {
5935            turso_soft_unreachable!("Page is a pointer map page and should not have an entry calculated this way", { "page": db_page_no_to_query });
5936            return Err(LimboError::InternalError(format!(
5937                "Page {db_page_no_to_query} is a pointer map page and should not have an entry calculated this way."
5938            )));
5939        }
5940
5941        let entry_index_on_page = (db_page_no_to_query - first_data_page_mapped) as usize;
5942        Ok(entry_index_on_page * PTRMAP_ENTRY_SIZE)
5943    }
5944}
5945
5946#[cfg(clt_turso_tests)]
5947mod tests {
5948    use crate::sync::Arc;
5949
5950    use crate::sync::RwLock;
5951
5952    use crate::io::{MemoryIO, OpenFlags, IO};
5953    use crate::storage::buffer_pool::BufferPool;
5954    use crate::storage::database::DatabaseFile;
5955    use crate::storage::page_cache::{PageCache, PageCacheKey};
5956    use crate::storage::wal::{Wal, WalFile, WalFileShared};
5957    use crate::util::IOExt;
5958    use arc_swap::ArcSwapOption;
5959
5960    use super::{default_page1, Page, PageRef, Pager};
5961
5962    fn pager_with_cache_capacity(cache_capacity: usize, database_pages: u32) -> Arc<Pager> {
5963        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5964        let buffer_pool = BufferPool::begin_init(&io, 4096 * 128);
5965
5966        let db_file = Arc::new(DatabaseFile::new(
5967            io.open_file(":memory:", OpenFlags::Create, false).unwrap(),
5968        ));
5969
5970        let wal_file = io.open_file("test.wal", OpenFlags::Create, false).unwrap();
5971        let wal_shared = WalFileShared::new_shared(wal_file).unwrap();
5972        let last_checksum_and_max_frame = wal_shared.read().last_checksum_and_max_frame();
5973        let wal: Arc<dyn Wal> = Arc::new(WalFile::new(
5974            io.clone(),
5975            wal_shared,
5976            last_checksum_and_max_frame,
5977            buffer_pool.clone(),
5978        ));
5979
5980        let init_page_1 = Arc::new(ArcSwapOption::new(Some(default_page1(None))));
5981        let pager = Arc::new(
5982            Pager::new(
5983                db_file,
5984                Some(wal),
5985                io,
5986                PageCache::new(cache_capacity),
5987                buffer_pool,
5988                Arc::new(crate::sync::Mutex::new(())),
5989                init_page_1,
5990            )
5991            .unwrap(),
5992        );
5993
5994        pager.io.step().unwrap();
5995        pager.io.block(|| pager.allocate_page1()).unwrap();
5996        for _ in 0..(database_pages - 1) {
5997            pager.io.block(|| pager.allocate_page()).unwrap();
5998        }
5999        pager
6000    }
6001
6002    /// The page cache capacity is a soft limit, as in SQLite: when every
6003    /// resident page is unevictable (held by cursors, dirty and unspillable),
6004    /// a read must still succeed by admitting the page over capacity instead
6005    /// of failing with Busy. The excess drains once pages become evictable.
6006    #[test]
6007    fn read_page_exceeds_capacity_when_cache_unevictable() {
6008        const CAP: usize = 5;
6009        let pager = pager_with_cache_capacity(CAP, 6);
6010
6011        // Allocating 6 pages against a 5-page cache forces a spill and evicts
6012        // at least one spilled page; find one that is no longer resident.
6013        let missing = (2..=6)
6014            .find(|&id| !pager.page_cache.read().contains_key(&PageCacheKey::new(id)))
6015            .expect("allocating 6 pages with a 5-page cache must evict at least one page")
6016            as i64;
6017
6018        // Hold strong references to every resident page so none can be
6019        // evicted or spilled.
6020        let held: Vec<PageRef> = (1..=6)
6021            .filter_map(|id| pager.cache_get(id).unwrap())
6022            .collect();
6023        assert_eq!(held.len(), CAP, "cache should be at capacity");
6024
6025        let (page, c) = pager.io.block(|| pager.read_page(missing)).unwrap();
6026        if let Some(c) = c {
6027            pager.io.wait_for_completion(c).unwrap();
6028        }
6029        while page.is_locked() {
6030            pager.io.step().unwrap();
6031        }
6032        assert_eq!(page.get().id as i64, missing);
6033        assert!(
6034            pager.page_cache.read().len() > CAP,
6035            "page must have been admitted over capacity"
6036        );
6037
6038        // Once the strong references are gone, the next insert drains the
6039        // excess back under capacity.
6040        drop(held);
6041        drop(page);
6042        pager.io.block(|| pager.allocate_page()).unwrap();
6043        assert!(
6044            pager.page_cache.read().len() <= CAP,
6045            "excess over capacity must drain once pages become evictable"
6046        );
6047    }
6048
6049    /// Same soft-limit guarantee for the write path: allocating a new page
6050    /// while the cache is full of unevictable pages must not fail.
6051    #[test]
6052    fn allocate_page_exceeds_capacity_when_cache_unevictable() {
6053        const CAP: usize = 5;
6054        let pager = pager_with_cache_capacity(CAP, 5);
6055
6056        // Hold strong references to all resident pages: dirty pages with
6057        // outstanding references can neither be spilled nor evicted.
6058        let held: Vec<PageRef> = (1..=5)
6059            .filter_map(|id| pager.cache_get(id).unwrap())
6060            .collect();
6061        assert_eq!(held.len(), CAP, "cache should be at capacity");
6062
6063        let page = pager.io.block(|| pager.allocate_page()).unwrap();
6064        assert_eq!(page.get().id, 6);
6065        assert!(
6066            pager.page_cache.read().len() > CAP,
6067            "page must have been admitted over capacity"
6068        );
6069    }
6070
6071    #[test]
6072    fn test_shared_cache() {
6073        // ensure cache can be shared between threads
6074        let cache = Arc::new(RwLock::new(PageCache::new(10)));
6075
6076        let thread = {
6077            let cache = cache.clone();
6078            std::thread::spawn(move || {
6079                let mut cache = cache.write();
6080                let page_key = PageCacheKey::new(1);
6081                let page = Page::new(1);
6082                // Set loaded so that we avoid eviction, as we evict the page from cache if it is not locked and not loaded
6083                page.set_loaded();
6084                cache.insert(page_key, Arc::new(page)).unwrap();
6085            })
6086        };
6087        let _ = thread.join();
6088        let mut cache = cache.write();
6089        let page_key = PageCacheKey::new(1);
6090        let page = cache.get(&page_key).unwrap();
6091        assert_eq!(page.unwrap().get().id, 1);
6092    }
6093}
6094
6095#[cfg(clt_turso_tests)]
6096#[cfg(not(clt_turso_feature = "omit_autovacuum"))]
6097mod ptrmap_tests {
6098    use crate::sync::Arc;
6099
6100    use super::ptrmap::*;
6101    use super::*;
6102    use crate::io::{MemoryIO, OpenFlags, IO};
6103    use crate::storage::buffer_pool::BufferPool;
6104    use crate::storage::database::DatabaseFile;
6105    use crate::storage::page_cache::PageCache;
6106    use crate::storage::pager::{default_page1, Pager};
6107    use crate::storage::sqlite3_ondisk::PageSize;
6108    use crate::storage::wal::{WalFile, WalFileShared};
6109    use arc_swap::ArcSwapOption;
6110
6111    pub fn run_until_done<T>(
6112        mut action: impl FnMut() -> Result<IOResult<T>>,
6113        pager: &Pager,
6114    ) -> Result<T> {
6115        loop {
6116            match action()? {
6117                IOResult::Done(res) => {
6118                    return Ok(res);
6119                }
6120                IOResult::IO(io) => io.wait(pager.io.as_ref())?,
6121            }
6122        }
6123    }
6124    // Helper to create a Pager for testing
6125    fn test_pager_setup(page_size: u32, initial_db_pages: u32) -> Pager {
6126        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
6127        let db_file: Arc<dyn DatabaseStorage> = Arc::new(DatabaseFile::new(
6128            io.open_file("test.db", OpenFlags::Create, true).unwrap(),
6129        ));
6130
6131        //  Construct interfaces for the pager
6132        let pages = initial_db_pages + 10;
6133        let sz = std::cmp::max(std::cmp::min(pages, 64), pages);
6134        let buffer_pool = BufferPool::begin_init(&io, (sz * page_size) as usize);
6135
6136        let wal_shared = WalFileShared::new_shared(
6137            io.open_file("test.db-wal", OpenFlags::Create, false)
6138                .unwrap(),
6139        )
6140        .unwrap();
6141        let last_checksum_and_max_frame = wal_shared.read().last_checksum_and_max_frame();
6142        let wal: Arc<dyn Wal> = Arc::new(WalFile::new(
6143            io.clone(),
6144            wal_shared,
6145            last_checksum_and_max_frame,
6146            buffer_pool.clone(),
6147        ));
6148
6149        // For new empty databases, init_page_1 must be Some(page) so allocate_page1() can be called
6150        let init_page_1 = Arc::new(ArcSwapOption::new(Some(default_page1(None))));
6151        let pager = Pager::new(
6152            db_file,
6153            Some(wal),
6154            io,
6155            PageCache::new(sz as usize),
6156            buffer_pool,
6157            Arc::new(Mutex::new(())),
6158            init_page_1,
6159        )
6160        .unwrap();
6161        run_until_done(|| pager.allocate_page1(), &pager).unwrap();
6162        {
6163            let page_cache = pager.page_cache.read();
6164            println!(
6165                "Cache Len: {} Cap: {}",
6166                page_cache.len(),
6167                page_cache.capacity()
6168            );
6169        }
6170        pager
6171            .persist_auto_vacuum_mode(AutoVacuumMode::Full)
6172            .unwrap();
6173
6174        //  Allocate all the pages as btree root pages
6175        const EXPECTED_FIRST_ROOT_PAGE_ID: u32 = 3; // page1 = 1,  first ptrmap page = 2, root page = 3
6176        for i in 0..initial_db_pages {
6177            let res = run_until_done(
6178                || pager.btree_create(&CreateBTreeFlags::new_table()),
6179                &pager,
6180            );
6181            {
6182                let page_cache = pager.page_cache.read();
6183                println!(
6184                    "i: {} Cache Len: {} Cap: {}",
6185                    i,
6186                    page_cache.len(),
6187                    page_cache.capacity()
6188                );
6189            }
6190            match res {
6191                Ok(root_page_id) => {
6192                    assert_eq!(root_page_id, EXPECTED_FIRST_ROOT_PAGE_ID + i);
6193                }
6194                Err(e) => {
6195                    panic!("test_pager_setup: btree_create failed: {e:?}");
6196                }
6197            }
6198        }
6199
6200        pager
6201    }
6202
6203    #[test]
6204    fn persist_auto_vacuum_mode_updates_fresh_header_without_dirty_pages() {
6205        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
6206        let db_file: Arc<dyn DatabaseStorage> = Arc::new(DatabaseFile::new(
6207            io.open_file("fresh-auto-vacuum.db", OpenFlags::Create, true)
6208                .unwrap(),
6209        ));
6210        let buffer_pool = BufferPool::begin_init(&io, 65536);
6211        let pager = Pager::new(
6212            db_file,
6213            None,
6214            io,
6215            PageCache::new(4),
6216            buffer_pool,
6217            Arc::new(Mutex::new(())),
6218            Arc::new(ArcSwapOption::new(Some(default_page1(None)))),
6219        )
6220        .unwrap();
6221
6222        pager
6223            .persist_auto_vacuum_mode(AutoVacuumMode::Incremental)
6224            .unwrap();
6225
6226        let IOResult::Done((largest_root_page, incremental_vacuum_enabled)) = pager
6227            .with_header(|header| {
6228                (
6229                    header.vacuum_mode_largest_root_page.get(),
6230                    header.incremental_vacuum_enabled.get(),
6231                )
6232            })
6233            .unwrap()
6234        else {
6235            panic!("fresh database header reads should not do any IO");
6236        };
6237
6238        assert_eq!(largest_root_page, 1);
6239        assert_eq!(incremental_vacuum_enabled, 1);
6240        assert_eq!(pager.get_auto_vacuum_mode(), AutoVacuumMode::Incremental);
6241        assert!(
6242            pager.dirty_pages.read().is_empty(),
6243            "fresh-db auto-vacuum setup must not leave dirty pages behind"
6244        );
6245    }
6246
6247    #[test]
6248    fn test_ptrmap_page_allocation() {
6249        let page_size = 4096;
6250        let initial_db_pages = 10;
6251        let pager = test_pager_setup(page_size, initial_db_pages);
6252
6253        // Page 5 should be mapped by ptrmap page 2.
6254        let db_page_to_update: u32 = 5;
6255        let expected_ptrmap_pg_no =
6256            get_ptrmap_page_no_for_db_page(db_page_to_update, page_size as usize);
6257        assert_eq!(expected_ptrmap_pg_no, FIRST_PTRMAP_PAGE_NO);
6258
6259        //  Ensure the pointer map page ref is created and loadable via the pager
6260        let ptrmap_page_ref = pager
6261            .io
6262            .block(|| pager.read_page(expected_ptrmap_pg_no as i64));
6263        assert!(ptrmap_page_ref.is_ok());
6264
6265        //  Ensure that the database header size is correctly reflected
6266        assert_eq!(
6267            pager
6268                .io
6269                .block(|| pager.with_header(|header| header.database_size))
6270                .unwrap()
6271                .get(),
6272            initial_db_pages + 2
6273        ); // (1+1) -> (header + ptrmap)
6274
6275        //  Read the entry from the ptrmap page and verify it
6276        let entry = pager
6277            .io
6278            .block(|| pager.ptrmap_get(db_page_to_update))
6279            .unwrap()
6280            .unwrap();
6281        assert_eq!(entry.entry_type, PtrmapType::RootPage);
6282        assert_eq!(entry.parent_page_no, 0);
6283    }
6284
6285    #[test]
6286    fn test_is_ptrmap_page_logic() {
6287        let page_size = PageSize::MIN as usize;
6288        let n_data_pages = entries_per_ptrmap_page(page_size);
6289        assert_eq!(n_data_pages, 102); //   512/5 = 102
6290
6291        assert!(!is_ptrmap_page(1, page_size)); // Header
6292        assert!(is_ptrmap_page(2, page_size)); // P0
6293        assert!(!is_ptrmap_page(3, page_size)); // D0_1
6294        assert!(!is_ptrmap_page(4, page_size)); // D0_2
6295        assert!(!is_ptrmap_page(5, page_size)); // D0_3
6296        assert!(is_ptrmap_page(105, page_size)); // P1
6297        assert!(!is_ptrmap_page(106, page_size)); // D1_1
6298        assert!(!is_ptrmap_page(107, page_size)); // D1_2
6299        assert!(!is_ptrmap_page(108, page_size)); // D1_3
6300        assert!(is_ptrmap_page(208, page_size)); // P2
6301    }
6302
6303    #[test]
6304    fn test_get_ptrmap_page_no() {
6305        let page_size = PageSize::MIN as usize; // Maps 103 data pages
6306
6307        // Test pages mapped by P0 (page 2)
6308        assert_eq!(get_ptrmap_page_no_for_db_page(3, page_size), 2); // D(3) -> P0(2)
6309        assert_eq!(get_ptrmap_page_no_for_db_page(4, page_size), 2); // D(4) -> P0(2)
6310        assert_eq!(get_ptrmap_page_no_for_db_page(5, page_size), 2); // D(5) -> P0(2)
6311        assert_eq!(get_ptrmap_page_no_for_db_page(104, page_size), 2); // D(104) -> P0(2)
6312
6313        assert_eq!(get_ptrmap_page_no_for_db_page(105, page_size), 105); // Page 105 is a pointer map page.
6314
6315        // Test pages mapped by P1 (page 6)
6316        assert_eq!(get_ptrmap_page_no_for_db_page(106, page_size), 105); // D(106) -> P1(105)
6317        assert_eq!(get_ptrmap_page_no_for_db_page(107, page_size), 105); // D(107) -> P1(105)
6318        assert_eq!(get_ptrmap_page_no_for_db_page(108, page_size), 105); // D(108) -> P1(105)
6319
6320        assert_eq!(get_ptrmap_page_no_for_db_page(208, page_size), 208); // Page 208 is a pointer map page.
6321    }
6322
6323    #[test]
6324    fn test_get_ptrmap_offset() {
6325        let page_size = PageSize::MIN as usize; //  Maps 103 data pages
6326
6327        assert_eq!(get_ptrmap_offset_in_page(3, 2, page_size).unwrap(), 0);
6328        assert_eq!(
6329            get_ptrmap_offset_in_page(4, 2, page_size).unwrap(),
6330            PTRMAP_ENTRY_SIZE
6331        );
6332        assert_eq!(
6333            get_ptrmap_offset_in_page(5, 2, page_size).unwrap(),
6334            2 * PTRMAP_ENTRY_SIZE
6335        );
6336
6337        //  P1 (page 105) maps D(106)...D(207)
6338        // D(106) is index 0 on P1. Offset 0.
6339        // D(107) is index 1 on P1. Offset 5.
6340        // D(108) is index 2 on P1. Offset 10.
6341        assert_eq!(get_ptrmap_offset_in_page(106, 105, page_size).unwrap(), 0);
6342        assert_eq!(
6343            get_ptrmap_offset_in_page(107, 105, page_size).unwrap(),
6344            PTRMAP_ENTRY_SIZE
6345        );
6346        assert_eq!(
6347            get_ptrmap_offset_in_page(108, 105, page_size).unwrap(),
6348            2 * PTRMAP_ENTRY_SIZE
6349        );
6350    }
6351
6352    /// Cache-hit fast path: `read_page_nonblock` must return `Done` with no
6353    /// disk-read completion and must not touch `pending_reads`.
6354    #[test]
6355    fn read_page_nonblock_cache_hit_returns_done() {
6356        let pager = test_pager_setup(4096, 10);
6357
6358        // Page 1 is unconditionally loaded into cache by `allocate_page1`.
6359        let res = pager.read_page(1).unwrap();
6360        match res {
6361            IOResult::Done((page, c)) => {
6362                assert_eq!(page.get().id, 1);
6363                assert!(
6364                    c.is_none(),
6365                    "cache hit must not return a disk-read completion"
6366                );
6367            }
6368            IOResult::IO(_) => panic!("cache hit should not yield"),
6369        }
6370        assert!(
6371            pager.pending_reads.read().is_empty(),
6372            "pending_reads must stay empty on cache-hit path"
6373        );
6374    }
6375
6376    /// Re-entry contract: if `pending_reads` already has a `PendingRead` for
6377    /// this page (as happens after a previous call yielded for spill), the
6378    /// next call must reuse that `(page, disk_read)` instead of allocating a
6379    /// new page and issuing a duplicate disk read.
6380    ///
6381    /// This test does NOT force a real spill yield — that requires an IO
6382    /// backend that returns non-finished completions, which we don't have at
6383    /// the core unit-test layer. We instead synthesize the post-yield state
6384    /// directly and assert the function honors it.
6385    #[test]
6386    fn read_page_nonblock_reentry_reuses_pending_entry() {
6387        let pager = test_pager_setup(4096, 10);
6388
6389        // Pick a page id well beyond the initialized DB so it is *not* in
6390        // the cache. We never actually issue IO against it (we short-circuit
6391        // via the pre-populated `pending_reads` entry), so the page id only
6392        // needs to be unique within the cache.
6393        let target_idx: i64 = 9999;
6394        assert!(
6395            pager.cache_get(target_idx as usize).unwrap().is_none(),
6396            "test precondition: target page must not be in cache"
6397        );
6398
6399        // Synthesize the state that would exist after a previous call had to
6400        // yield on spill: a `PendingRead` entry whose `page` is the
6401        // PageRef we already handed back to the caller, and whose
6402        // `disk_read` is the in-flight disk-read completion.
6403        let synthetic_page: PageRef = Arc::new(Page::new(target_idx));
6404        // Mark loaded so cache eviction logic treats it as a normal page; the
6405        // contents don't matter for this test.
6406        synthetic_page.set_loaded();
6407        let stub_disk_read = Completion::new_yield();
6408        pager.pending_reads.write().insert(
6409            target_idx,
6410            PendingRead {
6411                page: synthetic_page.clone(),
6412                disk_read: Some(stub_disk_read),
6413            },
6414        );
6415
6416        let res = pager.read_page(target_idx).unwrap();
6417        let (page, c) = match res {
6418            IOResult::Done(v) => v,
6419            IOResult::IO(_) => panic!(
6420                "with pending entry present and cache space available, \
6421                 read_page_nonblock should complete without yielding"
6422            ),
6423        };
6424
6425        assert!(
6426            Arc::ptr_eq(&page, &synthetic_page),
6427            "read_page_nonblock must reuse the PageRef from pending_reads, \
6428             not allocate a new page (this is the no-duplicate-IO invariant)"
6429        );
6430        assert!(
6431            c.is_some(),
6432            "the disk-read completion from pending_reads should be returned"
6433        );
6434        assert!(
6435            pager.pending_reads.read().get(&target_idx).is_none(),
6436            "pending_reads entry must be cleared once read_page_nonblock returns Done"
6437        );
6438    }
6439
6440    /// Concurrency contract: a page can be cache-resident while its disk read
6441    /// is still in flight (locked, not loaded) — `read_page` inserts into the
6442    /// shared cache before the read completes, and `PageCache::get` hands out
6443    /// such in-flight pages. A second reader hitting the cache-hit fast path
6444    /// must NOT receive that unloaded page with `None` (no completion to wait
6445    /// on); it must yield and re-enter until the read completes. Otherwise the
6446    /// caller reads a torn / uninitialized buffer, or races a writer filling
6447    /// the buffer underneath it.
6448    #[test]
6449    fn read_page_nonblock_inflight_cache_hit_yields_not_done() {
6450        let pager = test_pager_setup(4096, 10);
6451
6452        let target_idx: i64 = 9999;
6453        assert!(
6454            pager.cache_get(target_idx as usize).unwrap().is_none(),
6455            "test precondition: target page must not be in cache"
6456        );
6457
6458        // Synthesize an in-flight read that has already been published to the
6459        // shared cache: locked (a read is outstanding) but not loaded (the
6460        // buffer hasn't been filled yet). This is exactly the state a page is
6461        // in between `cache_insert` and the disk-read completion firing.
6462        let inflight: PageRef = Arc::new(Page::new(target_idx));
6463        inflight.set_locked();
6464        assert!(!inflight.is_loaded());
6465        pager
6466            .page_cache
6467            .write()
6468            .insert(PageCacheKey::new(target_idx as usize), inflight.clone())
6469            .unwrap();
6470
6471        // The fast path finds the page in cache but must refuse to return it
6472        // without a completion, because it is not yet loaded.
6473        match pager.read_page(target_idx).unwrap() {
6474            IOResult::IO(_) => {}
6475            IOResult::Done((page, c)) => panic!(
6476                "read_page handed out an in-flight (locked, unloaded) page on the \
6477                 cache-hit fast path: loaded={}, completion={}",
6478                page.is_loaded(),
6479                c.is_some()
6480            ),
6481        }
6482
6483        // Once the read completes (page becomes loaded), the same cache-hit
6484        // fast path returns Done with no completion, as before.
6485        inflight.set_loaded();
6486        match pager.read_page(target_idx).unwrap() {
6487            IOResult::Done((page, c)) => {
6488                assert!(Arc::ptr_eq(&page, &inflight));
6489                assert!(c.is_none(), "loaded cache hit must not return a completion");
6490            }
6491            IOResult::IO(_) => panic!("loaded cache hit must not yield"),
6492        }
6493    }
6494}
6495
6496#[cfg(all(clt_turso_tests, clt_turso_feature = "fs", host_shared_wal))]
6497mod checkpoint_phase_tests {
6498    use super::*;
6499    use crate::io::{PlatformIO, IO};
6500    use crate::storage::sqlite3_ondisk::DatabaseHeader;
6501    use crate::storage::wal::CheckpointMode;
6502    use crate::sync::atomic::Ordering;
6503    use crate::types::IOResult;
6504    use crate::Database;
6505
6506    /// Returns an IO backend that supports shared WAL coordination on the host.
6507    /// On Windows the default `PlatformIO` (`WindowsIO`) lacks the byte-locking
6508    /// and mapping primitives, so the experimental IOCP backend is used when
6509    /// the `experimental_win_iocp` feature is enabled.
6510    fn shared_wal_test_io() -> Arc<dyn IO> {
6511        #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
6512        {
6513            Arc::new(crate::WindowsIOCP::new().unwrap())
6514        }
6515        #[cfg(not(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp")))]
6516        {
6517            Arc::new(PlatformIO::new().unwrap())
6518        }
6519    }
6520
6521    fn open_checkpoint_test_database() -> (Arc<Database>, std::path::PathBuf) {
6522        let dir = tempfile::tempdir().unwrap().keep();
6523        let db_path = dir.join("test.db");
6524        {
6525            let connection = rusqlite::Connection::open(&db_path).unwrap();
6526            connection
6527                .pragma_update(None, "journal_mode", "wal")
6528                .unwrap();
6529        }
6530        let io = shared_wal_test_io();
6531        let db = Database::open_file_with_flags(
6532            io,
6533            db_path.to_str().unwrap(),
6534            crate::OpenFlags::default(),
6535            crate::DatabaseOpts::new().with_multiprocess_wal(true),
6536            None,
6537        )
6538        .unwrap();
6539        (db, dir)
6540    }
6541
6542    fn db_identity(db_path: &std::path::Path) -> (u32, u32) {
6543        let bytes = std::fs::read(db_path).unwrap();
6544        assert!(bytes.len() >= DatabaseHeader::SIZE);
6545        let db_size_pages = u32::from_be_bytes(bytes[28..32].try_into().unwrap());
6546        let crc = crc32c::crc32c(&bytes[..DatabaseHeader::SIZE]);
6547        (db_size_pages, crc)
6548    }
6549
6550    #[test]
6551    fn checkpoint_db_sync_completion_still_leaves_backfill_unpublished_until_proof_install() {
6552        let (db, dir) = open_checkpoint_test_database();
6553        let db_path = dir.join("test.db");
6554        let conn = db.connect().unwrap();
6555        conn.wal_auto_actions_disable();
6556        conn.execute("create table test(id integer primary key, value blob)")
6557            .unwrap();
6558        conn.execute("begin immediate").unwrap();
6559        for _ in 0..32 {
6560            conn.execute("insert into test(value) values (randomblob(2048))")
6561                .unwrap();
6562        }
6563        conn.execute("commit").unwrap();
6564        assert!(
6565            db.shared_wal
6566                .read()
6567                .metadata
6568                .max_frame
6569                .load(Ordering::SeqCst)
6570                > 1,
6571            "checkpoint setup requires more than one WAL frame"
6572        );
6573
6574        let pager = conn.pager.load();
6575        let mode = CheckpointMode::Passive {
6576            upper_bound_inclusive: Some(1),
6577        };
6578
6579        loop {
6580            match pager.checkpoint(mode, crate::SyncMode::Full, true).unwrap() {
6581                IOResult::Done(_) => {
6582                    panic!("checkpoint should not finish before we observe the post-sync gap")
6583                }
6584                IOResult::IO(io) => io.wait(pager.io.as_ref()).unwrap(),
6585            }
6586
6587            let state = pager.checkpoint_state.read();
6588            let Some(result) = state.result.as_ref() else {
6589                continue;
6590            };
6591            if matches!(state.phase, CheckpointPhase::ReadDbIdentity { .. })
6592                && result.db_sync_sent
6593                && !pager.syncing.load(Ordering::SeqCst)
6594            {
6595                break;
6596            }
6597        }
6598
6599        let authority = db.shared_wal_coordination().unwrap().unwrap();
6600        let snapshot_before_publish = authority.snapshot();
6601        let (db_size_pages, db_header_crc32c) = db_identity(&db_path);
6602        assert_eq!(
6603            snapshot_before_publish.nbackfills, 0,
6604            "DB sync completion alone must not publish positive nbackfills"
6605        );
6606        assert!(
6607            !authority.validate_backfill_proof(
6608                snapshot_before_publish,
6609                db_size_pages,
6610                db_header_crc32c
6611            ),
6612            "DB sync completion must still leave the durable backfill proof absent"
6613        );
6614
6615        let result = pager
6616            .io
6617            .block(|| pager.checkpoint(mode, crate::SyncMode::Full, true))
6618            .unwrap();
6619        assert!(
6620            result.wal_total_backfilled > 0 && !result.everything_backfilled(),
6621            "resumed checkpoint should complete the partial checkpoint after proof installation"
6622        );
6623
6624        let snapshot_after_publish = authority.snapshot();
6625        let (db_size_pages_after, db_header_crc32c_after) = db_identity(&db_path);
6626        assert!(
6627            snapshot_after_publish.nbackfills > 0,
6628            "proof installation step must publish positive nbackfills"
6629        );
6630        assert!(
6631            authority.validate_backfill_proof(
6632                snapshot_after_publish,
6633                db_size_pages_after,
6634                db_header_crc32c_after
6635            ),
6636            "resuming after the post-sync gap must install a valid durable backfill proof"
6637        );
6638    }
6639}