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
59const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe;
61const RESERVED_SPACE_NOT_SET: u16 = u16::MAX;
62
63#[cfg(clt_turso_feature = "test_helper")]
64static PENDING_BYTE: AtomicU32 = AtomicU32::new(0x40000000);
66
67#[cfg(not(clt_turso_feature = "test_helper"))]
68const 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 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 pub fn page(&self) -> &PageRef {
107 &self.0
108 }
109}
110
111pub struct PageInner {
112 pub flags: AtomicUsize,
113 pub id: usize,
114 pub pin_count: AtomicUsize,
123 pub wal_tag: AtomicU64,
126 pub buffer: Option<Arc<Buffer>>,
128 pub overflow_cells: Vec<OverflowCell>,
130}
131
132impl PageInner {
134 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 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 #[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 #[inline]
170 pub fn offset(&self) -> usize {
171 if self.id == 1 {
172 DatabaseHeader::SIZE
173 } else {
174 0
175 }
176 }
177
178 #[inline]
180 fn read_u8(&self, pos: usize) -> u8 {
181 let buf = self.as_ptr();
182 buf[self.offset() + pos]
183 }
184
185 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 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 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
689pub type PageContent = PageInner;
691
692pub const TAG_UNSET: u64 = u64::MAX;
694pub const TAG_WRITE_PENDING: u64 = u64::MAX - 1;
697
698const 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
724unsafe impl Send for Page {}
727unsafe impl Sync for Page {}
728crate::assert::assert_send_sync!(Page);
729
730pub type PageRef = Arc<Page>;
733
734const PAGE_LOCKED: usize = 0b010;
736const PAGE_DIRTY: usize = 0b1000;
738const PAGE_LOADED: usize = 0b10000;
740const 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 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 pub fn set_dirty(&self) {
798 tracing::debug!("set_dirty(page={})", self.get().id);
799 self.clear_wal_tag();
800 self.get().flags.fetch_and(!PAGE_SPILLED, Ordering::Release);
802 self.get().flags.fetch_or(PAGE_DIRTY, Ordering::Release);
803 }
804
805 #[inline]
806 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 #[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 #[inline]
823 pub fn is_spilled(&self) -> bool {
824 self.get().flags.load(Ordering::Acquire) & PAGE_SPILLED != 0
825 }
826
827 #[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 #[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 #[inline]
866 pub fn pin(&self) {
867 self.get().pin_count.fetch_add(1, Ordering::SeqCst);
868 }
869
870 #[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 #[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 #[inline]
901 pub fn is_pinned(&self) -> bool {
902 self.get().pin_count.load(Ordering::Acquire) > 0
903 }
904
905 #[inline]
906 pub fn set_wal_tag(&self, frame: u64, epoch: u32) {
910 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 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 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 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 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 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)]
988enum CommitState {
990 PrepareWal,
992 PrepareWalSync,
994 GetDbSize,
996 ScanAndIssueReads { db_size: u32 },
998 WaitBatchedReads { db_size: u32 },
1000 PrepareFrames { db_size: u32 },
1002 WaitWrites,
1004 WaitSync,
1012 WalCommitDone,
1016 AutoCheckpoint,
1019}
1020
1021#[derive(Debug, Default)]
1022struct CheckpointState {
1023 phase: CheckpointPhase,
1024 result: Option<CheckpointResult>,
1026 mode: Option<CheckpointMode>,
1028 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 TruncateDbFile {
1051 sync_mode: crate::SyncMode,
1052 clear_page_cache: bool,
1053 page1_invalidated: bool,
1056 },
1057 SyncDbFile { clear_page_cache: bool },
1059 ReadDbIdentity {
1061 clear_page_cache: bool,
1062 read: PendingCheckpointDbIdentityRead,
1063 },
1064 SyncBackfillProof {
1066 clear_page_cache: bool,
1067 max_frame: u64,
1068 },
1069 PublishBackfill {
1071 clear_page_cache: bool,
1072 max_frame: u64,
1073 },
1074 TruncateWalFile { clear_page_cache: bool },
1077 Finalize { clear_page_cache: bool },
1079}
1080
1081pub enum BtreePageAllocMode {
1087 Any,
1089 Exact(u32),
1091 Le(u32),
1093}
1094
1095struct 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
1106enum PageSource {
1108 Cached(usize),
1110 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 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#[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 Commit,
1223 Release,
1225 NotFound,
1227}
1228
1229#[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: AtomicU64,
1252 write_offset: AtomicU64,
1254 page_bitmap: RwLock<RoaringBitmap>,
1256 db_size: AtomicU32,
1260 wal_pos: RwLock<Option<SavepointWalPos>>,
1266 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
1332pub struct Pager {
1336 pub db_file: Arc<dyn DatabaseStorage>,
1338 pub(crate) wal: Option<Arc<dyn Wal>>,
1341 page_cache: Arc<RwLock<PageCache>>,
1343 pub buffer_pool: Arc<BufferPool>,
1345 pub io: Arc<dyn crate::io::IO>,
1347 pending_reads: RwLock<HashMap<i64, PendingRead>>,
1353 #[cfg(clt_turso_tests)]
1354 spill_yield: SpillYieldHook,
1355 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 init_lock: Arc<Mutex<()>>,
1365 allocate_page_state: RwLock<AllocatePageState>,
1367 allocate_page1_state: RwLock<AllocatePage1State>,
1369 pub(crate) page_size: AtomicU32,
1373 reserved_space: AtomicU16,
1374 schema_cookie: AtomicU64,
1379 free_page_state: RwLock<FreePageState>,
1380 spill_state: RwLock<SpillState>,
1382 cacheflush_state: RwLock<CacheFlushState>,
1384 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 enable_encryption: AtomicBool,
1392 init_page_1: Arc<ArcSwapOption<Page>>,
1394 #[cfg(target_vendor = "apple")]
1397 sync_type: AtomicFileSyncType,
1398 pub(crate) cursor_registry: Mutex<rustc_hash::FxHashMap<i64, Vec<RegisteredCursor>>>,
1402}
1403
1404#[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 #[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#[derive(Clone)]
1450struct PendingRead {
1451 page: PageRef,
1452 disk_read: Option<Completion>,
1455}
1456
1457#[cfg(clt_turso_tests)]
1461struct SpillYieldHook {
1462 target: std::sync::atomic::AtomicI64,
1464 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 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 ptrmap_get_state: PtrMapGetState,
1505 ptrmap_put_state: PtrMapPutState,
1507 btree_create_vacuum_full_state: BtreeCreateVacuumFullState,
1508}
1509
1510#[derive(Debug, Clone)]
1511enum AllocatePageState {
1512 Start,
1513 SearchAvailableFreeListLeaf {
1519 trunk_page: PageRef,
1520 },
1521 ReuseFreelistLeaf {
1523 trunk_page: PageRef,
1524 leaf_page: PageRef,
1525 number_of_freelist_leaves: u32,
1526 },
1527 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#[derive(Debug, Default, Clone)]
1550enum SpillState {
1551 #[default]
1552 Idle,
1554 PreparingWalStart {
1558 pages: Vec<PinGuard>,
1559 completion: Completion,
1560 },
1561 PreparingWalFinish {
1564 pages: Vec<PinGuard>,
1565 completion: Completion,
1566 },
1567 WritingToWal {
1569 pages: Vec<PinGuard>,
1571 completions: Vec<Completion>,
1573 },
1574 WritingToDisk {
1576 pages: Vec<PinGuard>,
1578 completions: Vec<Completion>,
1580 },
1581}
1582enum CacheFlushStep {
1583 Yield(CacheFlushState, IOCompletions),
1585 Continue(CacheFlushState),
1587 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), 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 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 if bucket.len() >= 2 {
1695 for &peer in bucket.iter() {
1696 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 if bucket.len() == 1 {
1712 let surviving = bucket[0];
1713 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 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 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 unsafe { peer.as_mut().invalidate_btree_cache() };
1756 }
1757 }
1758
1759 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 unsafe { peer.as_mut().invalidate_btree_cache() };
1767 }
1768 }
1769
1770 #[cfg(target_vendor = "apple")]
1773 #[inline]
1774 pub fn get_sync_type(&self) -> FileSyncType {
1775 self.sync_type.get()
1776 }
1777
1778 #[cfg(not(target_vendor = "apple"))]
1781 #[inline]
1782 pub fn get_sync_type(&self) -> FileSyncType {
1783 FileSyncType::Fsync
1784 }
1785
1786 #[cfg(target_vendor = "apple")]
1788 pub fn set_sync_type(&self, value: FileSyncType) {
1789 self.sync_type.set(value);
1790 }
1791
1792 #[cfg(not(target_vendor = "apple"))]
1794 pub fn set_sync_type(&self, _value: FileSyncType) {
1795 }
1797
1798 pub fn init_page_1(&self) -> Arc<ArcSwapOption<Page>> {
1799 self.init_page_1.clone()
1800 }
1801
1802 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 let Some(page1) = self.init_page_1.load_full() {
1812 return Ok(IOResult::Done(page1));
1813 }
1814
1815 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 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 pub fn set_spill_enabled(&self, enabled: bool) {
1849 self.page_cache.write().set_spill_enabled(enabled);
1850 }
1851 pub fn get_spill_enabled(&self) -> bool {
1853 self.page_cache.read().is_spill_enabled()
1854 }
1855
1856 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 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 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 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 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 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 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 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 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 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 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 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 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 {
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 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 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 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 pub fn pending_byte_page_id(&self) -> Option<u32> {
2322 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 pub fn get_max_page_count(&self) -> u32 {
2331 self.max_page_count.load(Ordering::SeqCst)
2332 }
2333
2334 pub fn set_max_page_count(&self, new_max: u32) -> crate::Result<IOResult<u32>> {
2337 let current_page_count =
2339 return_if_io!(self.with_header(|header| header.database_size.get()));
2340
2341 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 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 self.dirty_pages.write().clear();
2382 }
2383
2384 self.set_auto_vacuum_mode(mode);
2385 Ok(())
2386 }
2387
2388 #[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 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 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 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 #[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 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 #[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 #[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 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 }
2664
2665 self.vacuum_state.write().btree_create_vacuum_full_state =
2667 BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id };
2668 }
2669 BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id } => {
2670 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 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 let contents = page.get_contents();
2701 let buf = contents.as_ptr();
2702 buf.fill(0);
2703
2704 Ok(IOResult::Done(page))
2705 }
2706
2707 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 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 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 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 self.dirty_pages.write().clear();
2798 Ok(())
2799 }
2800
2801 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 self.dirty_pages.write().clear();
2815 Ok(())
2816 }
2817
2818 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 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 pub fn set_page_size(&self, size: PageSize) {
2849 self.page_size.store(size.get(), Ordering::SeqCst);
2850 }
2851
2852 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 pub fn set_reserved_space(&self, space: u8) {
2864 self.reserved_space.store(space as u16, Ordering::SeqCst);
2865 }
2866
2867 const SCHEMA_COOKIE_NOT_SET: u64 = u64::MAX;
2869
2870 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 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 pub fn get_schema_cookie(&self) -> Result<IOResult<u32>> {
2888 if let Some(cookie) = self.get_schema_cookie_cached() {
2890 return Ok(IOResult::Done(cookie));
2891 }
2892 self.with_header(|header| header.schema_cookie.get())
2894 }
2895
2896 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 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 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 self.clear_page_cache(false);
2931 self.set_schema_cookie(None);
2933 }
2934 Ok(())
2935 }
2936
2937 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 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 io_yield_one!(Completion::new_yield());
2957 }
2958 Ok(IOResult::Done(()))
2959 }
2960
2961 #[inline(always)]
2962 #[instrument(skip_all, level = Level::DEBUG)]
2963 pub fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<IOResult<()>> {
2970 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 self.materialize_savepoint_wal_positions();
2980 Ok(IOResult::Done(()))
2981 }
2982
2983 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 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 self.clear_page_cache(false);
3023 self.set_schema_cookie(None);
3024 Ok(IOResult::Done(()))
3025 }
3026
3027 #[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 return Ok(IOResult::Done(()));
3039 }
3040 let Some(wal) = self.wal.as_ref() else {
3041 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 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 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 return;
3118 }
3119 let Some(wal) = self.wal.as_ref() else {
3120 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 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 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 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 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 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 #[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 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 #[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 (pending.page, pending.disk_read)
3278 } else {
3279 {
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 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 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 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 }
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 #[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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 Ok(CacheFlushStep::Done(state.completions))
3672 }
3673
3674 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 #[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 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 #[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 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 match wal.prepare_wal_start(page_sz)? {
3784 Some(c) => {
3785 *self.spill_state.write() = SpillState::PreparingWalStart {
3786 pages,
3787 completion: c,
3788 };
3789 continue;
3792 }
3793 None => {
3794 return self.spill_append_frames_to_wal(pages);
3796 }
3797 }
3798 } else {
3799 let mut group = CompletionGroup::new(|_| {});
3800 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 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 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 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 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 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 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 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 {
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 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 fn finish_ephemeral_spill(&self, pages: &[PinGuard]) {
3967 for page in pages {
3968 let tag = page.get().wal_tag.load(Ordering::Acquire);
3969 if tag != TAG_UNSET {
3972 page.clear_dirty();
3973 }
3974 }
3975 }
3976 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 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 }
4010 IOResult::IO(completion) => {
4011 return Ok(IOResult::IO(completion));
4012 }
4013 }
4014 }
4015 Ok(IOResult::Done(()))
4016 }
4017
4018 #[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 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 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 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 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 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 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 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 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 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 commit_info.state = CommitState::WaitSync;
4289 }
4290 CommitState::WaitSync => {
4298 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 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 if !sync_c.finished() {
4324 io_yield_one!(sync_c);
4325 }
4326 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 return Ok(IOResult::Done(()));
4351 }
4352 commit_info.state = CommitState::WalCommitDone;
4353 }
4354 CommitState::WalCommitDone => {
4355 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 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 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 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 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 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 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 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 && 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 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 state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache };
4668 } else {
4669 state.phase = CheckpointPhase::SyncDbFile { clear_page_cache };
4671 }
4672 continue;
4673 }
4674 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 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 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 let mut state = self.checkpoint_state.write();
4706 if sync_mode == crate::SyncMode::Off {
4707 state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache };
4709 } else {
4710 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 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 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 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 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 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 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 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 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 Ok(())
5021 }
5022
5023 #[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 #[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 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 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 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 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 page.unpin();
5150 break;
5151 }
5152 *state = FreePageState::NewTrunk { page: page.clone() };
5154 }
5155 FreePageState::NewTrunk { page } => {
5156 turso_assert!(page.is_loaded(), "page should be loaded");
5157 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 contents
5166 .write_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR, trunk_page_id);
5167 contents.write_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT, 0);
5169 header.freelist_trunk_page = (page_id as u32).into();
5171 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 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 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 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 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 #[allow(clippy::readonly_write_lock)]
5274 #[instrument(skip_all, level = Level::DEBUG)]
5275 pub fn allocate_page(&self) -> Result<IOResult<PageRef>> {
5276 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 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 new_db_size += 1;
5306 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 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 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 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 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 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 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 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 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 self.add_dirty(trunk_page)?;
5446
5447 let remaining_leaves_count = (*number_of_freelist_leaves - 1) as usize;
5449 {
5450 let buf = page_contents.as_ptr();
5451 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 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 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 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 new_db_size += 1;
5489 }
5490
5491 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 let page = allocate_new_page(new_db_size as i64, &self.buffer_pool);
5501 {
5502 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 if dirty_page_must_exist {
5529 turso_assert!(page.is_dirty(), "page must be dirty for upsert", { "page_id": id });
5530 }
5531 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 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 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 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 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 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 self.clear_page_cache(false);
5672 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 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 let mut default_header = DatabaseHeader::default();
5712
5713 if let Some(cipher) = cipher {
5714 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, (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#[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 pub const PTRMAP_ENTRY_SIZE: usize = 5;
5808 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 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 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 pub fn is_ptrmap_page(db_page_no: u32, page_size: usize) -> bool {
5885 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 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 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 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 #[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 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 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 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 #[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 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 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 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 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 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 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 const EXPECTED_FIRST_ROOT_PAGE_ID: u32 = 3; 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 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 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 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 ); 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); assert!(!is_ptrmap_page(1, page_size)); assert!(is_ptrmap_page(2, page_size)); assert!(!is_ptrmap_page(3, page_size)); assert!(!is_ptrmap_page(4, page_size)); assert!(!is_ptrmap_page(5, page_size)); assert!(is_ptrmap_page(105, page_size)); assert!(!is_ptrmap_page(106, page_size)); assert!(!is_ptrmap_page(107, page_size)); assert!(!is_ptrmap_page(108, page_size)); assert!(is_ptrmap_page(208, page_size)); }
6302
6303 #[test]
6304 fn test_get_ptrmap_page_no() {
6305 let page_size = PageSize::MIN as usize; assert_eq!(get_ptrmap_page_no_for_db_page(3, page_size), 2); assert_eq!(get_ptrmap_page_no_for_db_page(4, page_size), 2); assert_eq!(get_ptrmap_page_no_for_db_page(5, page_size), 2); assert_eq!(get_ptrmap_page_no_for_db_page(104, page_size), 2); assert_eq!(get_ptrmap_page_no_for_db_page(105, page_size), 105); assert_eq!(get_ptrmap_page_no_for_db_page(106, page_size), 105); assert_eq!(get_ptrmap_page_no_for_db_page(107, page_size), 105); assert_eq!(get_ptrmap_page_no_for_db_page(108, page_size), 105); assert_eq!(get_ptrmap_page_no_for_db_page(208, page_size), 208); }
6322
6323 #[test]
6324 fn test_get_ptrmap_offset() {
6325 let page_size = PageSize::MIN as usize; 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 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 #[test]
6355 fn read_page_nonblock_cache_hit_returns_done() {
6356 let pager = test_pager_setup(4096, 10);
6357
6358 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 #[test]
6386 fn read_page_nonblock_reentry_reuses_pending_entry() {
6387 let pager = test_pager_setup(4096, 10);
6388
6389 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 let synthetic_page: PageRef = Arc::new(Page::new(target_idx));
6404 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 #[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 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 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 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 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}