1use crate::alloc::{
2 ConcurrentAllocator, TryReserveError, TursoAllocator, TursoIteratorExt, TursoVecExt, Vec,
3 ALLOC_ERR_MSG,
4};
5use crate::mvcc::clock::LogicalClock;
6use crate::mvcc::database::{
7 DeleteRowStateMachine, MVTableId, MvStore, Row, RowID, RowKey, RowVersion, SortableIndexKey,
8 TxTimestampOrID, WalPos, WriteRowStateMachine, MVCC_META_KEY_PERSISTENT_TX_TS_MAX,
9 MVCC_META_TABLE_NAME, SQLITE_SCHEMA_MVCC_TABLE_ID,
10};
11#[cfg(any(clt_turso_tests, injected_yields))]
12use crate::mvcc::yield_hooks::{ProvidesYieldContext, YieldContext, YieldPointMarker};
13use crate::mvcc::yield_points::{inject_transition_failure, inject_transition_yield};
14use crate::schema::{Index, Schema};
15use crate::state_machine::{StateMachine, StateTransition, TransitionResult};
16use crate::storage::btree::{BTreeCursor, CursorTrait};
17use crate::storage::pager::CreateBTreeFlags;
18use crate::storage::sqlite3_ondisk::DatabaseHeader;
19use crate::storage::wal::{CheckpointMode, TursoRwLock, WalAutoActions};
20use crate::sync::atomic::Ordering;
21use crate::sync::Arc;
22use crate::sync::RwLock;
23use crate::types::{IOCompletions, IOResult, ImmutableRecord, ImmutableRecordRef};
24use crate::{turso_assert, turso_assert_eq};
25use crate::{
26 CheckpointResult, Completion, Connection, IOExt, LimboError, Numeric, Pager, Result, SyncMode,
27 TransactionState, Value, ValueRef,
28};
29use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
30use std::num::NonZeroU64;
31use std::ops::Bound;
32#[cfg(any(clt_turso_tests, injected_yields))]
33use strum::EnumCount;
34
35use super::lookup_tx_state;
36const COLLECT_PREEMPTION_THRESHOLD: usize = 1024;
37
38macro_rules! with_mvcc_checkpoint_allocation_site {
39 ($site:ident, $expr:expr) => {{
40 #[cfg(clt_turso_feature = "allocation_metric")]
41 let _turso_allocation_site_guard =
42 crate::alloc::enter_allocation_site(crate::alloc::MvccCheckpointAllocationSite::$site);
43 $expr
44 }};
45}
46
47const SQLITE_SCHEMA_ROOT_PAGE: i64 = 1;
49const SQLITE_SCHEMA_COLUMN_COUNT: usize = 5;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CheckpointState {
54 PrepareCheckpoint,
55 AcquireLock,
56 BuildLocalSchemaView,
57 CollectTableRows,
58 CollectIndexRows,
59 BeginPagerTxn,
60 WriteRow {
61 write_set_index: usize,
62 requires_seek: bool,
63 },
64 WriteRowStateMachine {
65 write_set_index: usize,
66 },
67 DeleteRowStateMachine {
68 write_set_index: usize,
69 },
70 WriteIndexRow {
71 index_write_set_index: usize,
72 requires_seek: bool,
73 },
74 WriteIndexRowStateMachine {
75 index_write_set_index: usize,
76 },
77 DeleteIndexRowStateMachine {
78 index_write_set_index: usize,
79 },
80 CompactSequences,
90 CommitPagerTxn,
91 CheckpointWal,
92 SyncDbFile,
96 TruncateLogicalLog,
97 FsyncLogicalLog,
98 TruncateWal,
100 GcTableRows {
101 next_index: usize,
102 lwm: u64,
103 },
104 GcIndexRows {
105 next_index: usize,
106 lwm: u64,
107 },
108 Finalize,
109}
110
111#[cfg(any(clt_turso_tests, injected_yields))]
112#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumCount)]
113#[repr(u8)]
114pub(crate) enum CheckpointYieldPoint {
115 BeforeAcquireLock,
116 AfterDurableBoundaryAdvanced,
117 AfterCollectTableRows,
118}
119
120#[cfg(any(clt_turso_tests, injected_yields))]
121impl YieldPointMarker for CheckpointYieldPoint {
122 const POINT_COUNT: u8 = Self::COUNT as u8;
123
124 fn ordinal(self) -> u8 {
125 self as u8
126 }
127}
128
129#[cfg(any(clt_turso_tests, injected_yields))]
130fn checkpoint_yield_key() -> u64 {
131 const CHECKPOINT_SELECTION_TAG: u64 = 0xC4EC_9011_C4EC_9011;
132 CHECKPOINT_SELECTION_TAG
133}
134
135enum RootMapOp {
137 Alloc { id: MVTableId, root: u64 },
139 Retire { id: MVTableId, end_ts: u64 },
141 Remove { id: MVTableId },
143}
144
145pub struct LockStates {
148 blocking_checkpoint_lock_held: bool,
149 pager_read_tx: bool,
150 pager_write_tx: bool,
151}
152
153pub struct CheckpointStateMachine<Clock: LogicalClock, A: ConcurrentAllocator = TursoAllocator> {
169 state: CheckpointState,
171 lock_states: LockStates,
174 durable_txid_max_old: Option<NonZeroU64>,
176 durable_txid_max_new: u64,
178 pager: Arc<Pager>,
180 mvstore: Arc<MvStore<Clock, A>>,
182 connection: Arc<Connection>,
184 database_id: usize,
186 #[cfg(any(clt_turso_tests, injected_yields))]
187 yield_instance_id: u64,
188 checkpoint_lock: Arc<TursoRwLock>,
190 write_set: Vec<(RowVersion, Option<SpecialWrite>)>,
193 write_row_state_machine: Option<StateMachine<WriteRowStateMachine>>,
195 delete_row_state_machine: Option<StateMachine<DeleteRowStateMachine>>,
197 cursors: HashMap<u64, Arc<RwLock<BTreeCursor>>>,
199 created_btrees: HashMap<i64, (MVTableId, RowVersion)>,
202 destroyed_tables: HashSet<MVTableId>,
204 destroyed_indexes: HashSet<MVTableId>,
206 index_write_set: Vec<(MVTableId, RowVersion, bool)>,
208 index_id_to_index: HashMap<MVTableId, Arc<Index>>,
211 checkpoint_result: Option<CheckpointResult>,
213 update_transaction_state: bool,
217 sync_mode: SyncMode,
219 mode: CheckpointMode,
222 mvcc_meta_table: Option<(MVTableId, usize)>,
224 durable_mvcc_metadata: bool,
226 staged_checkpoint_header: Option<DatabaseHeader>,
228 header_staged_for_commit: bool,
230 pager_commit_done: bool,
233 pending_rootmap_ops: Vec<RootMapOp>,
235 pending_alloc_roots: std::collections::HashMap<MVTableId, u64>,
237 collect_table_cursor: Option<RowID>,
238 collect_index_tableid_cursor: Option<MVTableId>,
239 collect_index_key_cursor: Option<Arc<SortableIndexKey>>,
240 seq_compact: Option<SeqCompactDriver<Clock, A>>,
243 pending_seq_deletes: Vec<(RowID, usize)>,
245 snapshot_ts: u64,
247 build_local_schema_sm: Option<StateMachine<BuildLocalSchemaViewStateMachine<Clock, A>>>,
248 build_local_schema_began_read_tx: bool,
249 local_schema: Option<Arc<Schema>>,
251 owns_checkpoint_in_progress: bool,
252 staged_roots: Vec<MVTableId>,
254}
255
256#[derive(Debug, Clone, Copy)]
258struct SeqCompaction {
259 backing_root: i64,
260 backing_num_cols: usize,
261 table_id: MVTableId,
268 increment_positive: bool,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282enum SeqCompactPhase {
283 SeekWatermark,
286 ReadWatermarkRowid,
290 ScanRewind,
292 ScanReadRowid,
294 ScanDelete,
297 ScanNext,
301}
302
303struct SeqCompactDriver<Clock: LogicalClock, A: ConcurrentAllocator = TursoAllocator> {
317 pending: Vec<SeqCompaction>,
319 current_idx: usize,
321 cursor: Option<BTreeCursor>,
324 phase: SeqCompactPhase,
326 watermark_key: Option<i64>,
329 pending_delete_rowid: Option<i64>,
336 pager: Arc<Pager>,
339 mvstore: Arc<MvStore<Clock, A>>,
342 passive: bool,
344 compacted: Vec<(RowID, usize)>,
347}
348
349#[cfg(any(clt_turso_tests, injected_yields))]
350impl<Clock: LogicalClock, A: ConcurrentAllocator> ProvidesYieldContext
351 for CheckpointStateMachine<Clock, A>
352{
353 fn yield_context(&self) -> YieldContext {
354 YieldContext::new(
355 self.connection.yield_injector(),
356 self.connection.failure_injector(),
357 self.yield_instance_id,
358 checkpoint_yield_key(),
359 )
360 }
361}
362
363#[derive(Debug, PartialEq, Eq, Clone, Copy)]
364pub enum SpecialWrite {
367 BTreeCreate {
368 table_id: MVTableId,
369 sqlite_schema_rowid: i64,
370 },
371 BTreeDestroy {
372 table_id: MVTableId,
373 root_page: u64,
374 num_columns: usize,
375 },
376 BTreeCreateIndex {
377 index_id: MVTableId,
378 sqlite_schema_rowid: i64,
379 },
380 BTreeDestroyIndex {
381 index_id: MVTableId,
382 root_page: u64,
383 num_columns: usize,
384 },
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388enum SqliteSchemaBtreeKind {
389 Table,
390 Index,
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub struct SqliteSchemaBtreeIdentity {
395 kind: SqliteSchemaBtreeKind,
396 pub root_page: i64,
397}
398
399pub fn sqlite_schema_btree_identity(version: &RowVersion) -> Option<SqliteSchemaBtreeIdentity> {
403 if version.row.id.table_id != SQLITE_SCHEMA_MVCC_TABLE_ID {
404 return None;
405 }
406
407 if version.row.payload().is_empty() {
411 return None;
412 }
413
414 let row_data = ImmutableRecordRef::from_bin_record(version.row.payload());
415 let Ok((col0, col3)) = row_data.get_two_values(0, 3) else {
416 return None;
417 };
418
419 let kind = match col0 {
420 ValueRef::Text(type_str) => match type_str.as_str() {
421 "table" => SqliteSchemaBtreeKind::Table,
422 "index" => SqliteSchemaBtreeKind::Index,
423 _ => return None,
424 },
425 _ => panic!("sqlite_schema.type column must be TEXT, got {col0:?}"),
426 };
427
428 let ValueRef::Numeric(Numeric::Integer(root_page)) = col3 else {
429 panic!("sqlite_schema.rootpage column must be INTEGER, got {col3:?}");
430 };
431
432 if root_page == 0 {
433 return None;
434 }
435
436 Some(SqliteSchemaBtreeIdentity { kind, root_page })
437}
438
439fn sqlite_schema_versions_refer_to_btree(lhs: &RowVersion, rhs: &RowVersion) -> bool {
440 sqlite_schema_btree_identity(lhs)
441 .zip(sqlite_schema_btree_identity(rhs))
442 .is_some_and(|(lhs_id, rhs_id)| lhs_id == rhs_id)
443}
444
445fn is_schema_metadata_only_rewrite(current: &RowVersion, next: Option<&RowVersion>) -> bool {
454 if current.end().is_none() {
455 return false;
456 }
457
458 let Some(_current_identity) = sqlite_schema_btree_identity(current) else {
459 return false;
460 };
461
462 match next {
463 Some(next) => !sqlite_schema_versions_refer_to_btree(current, next),
464 None => true,
465 }
466}
467
468impl<Clock: LogicalClock, A: ConcurrentAllocator> SeqCompactDriver<Clock, A> {
469 fn step(&mut self) -> Result<IOResult<()>> {
474 loop {
475 let Some(seq) = self.pending.get(self.current_idx).copied() else {
476 return Ok(IOResult::Done(()));
477 };
478 if self.cursor.is_none() {
479 self.cursor = Some(BTreeCursor::new_table(
480 self.pager.clone(),
481 seq.backing_root,
482 seq.backing_num_cols,
483 ));
484 self.phase = SeqCompactPhase::SeekWatermark;
485 self.watermark_key = None;
486 self.pending_delete_rowid = None;
487 }
488 let cursor = self
489 .cursor
490 .as_mut()
491 .expect("cursor must be set in active compaction");
492 match self.phase {
493 SeqCompactPhase::SeekWatermark => {
494 let r = if seq.increment_positive {
495 cursor.last()?
496 } else {
497 cursor.rewind()?
498 };
499 if let IOResult::IO(io) = r {
500 return Ok(IOResult::IO(io));
501 }
502 self.phase = SeqCompactPhase::ReadWatermarkRowid;
503 }
504 SeqCompactPhase::ReadWatermarkRowid => {
505 let r = cursor.rowid()?;
506 let key = match r {
507 IOResult::IO(io) => return Ok(IOResult::IO(io)),
508 IOResult::Done(opt) => opt,
509 };
510 match key {
511 Some(k) => {
512 self.watermark_key = Some(k);
513 self.phase = SeqCompactPhase::ScanRewind;
514 }
515 None => {
516 self.advance_to_next_sequence();
518 }
519 }
520 }
521 SeqCompactPhase::ScanRewind => {
522 let r = cursor.rewind()?;
523 if let IOResult::IO(io) = r {
524 return Ok(IOResult::IO(io));
525 }
526 self.phase = SeqCompactPhase::ScanReadRowid;
527 }
528 SeqCompactPhase::ScanReadRowid => {
529 let r = cursor.rowid()?;
530 let key = match r {
531 IOResult::IO(io) => return Ok(IOResult::IO(io)),
532 IOResult::Done(opt) => opt,
533 };
534 match key {
535 Some(k) if Some(k) == self.watermark_key => {
536 self.phase = SeqCompactPhase::ScanNext;
537 }
538 Some(k) => {
539 if self.passive {
540 self.compacted.push((
545 RowID {
546 table_id: seq.table_id,
547 row_id: RowKey::Int(k),
548 },
549 seq.backing_num_cols,
550 ));
551 self.phase = SeqCompactPhase::ScanNext;
552 } else {
553 self.pending_delete_rowid = Some(k);
561 self.phase = SeqCompactPhase::ScanDelete;
562 }
563 }
564 None => {
565 self.advance_to_next_sequence();
566 }
567 }
568 }
569 SeqCompactPhase::ScanDelete => {
570 let r = cursor.delete()?;
571 if let IOResult::IO(io) = r {
572 return Ok(IOResult::IO(io));
573 }
574 let rowid = self
585 .pending_delete_rowid
586 .take()
587 .expect("pending_delete_rowid must be set when ScanDelete completes");
588 self.mvstore.purge_row_versions_during_checkpoint(RowID {
589 table_id: seq.table_id,
590 row_id: RowKey::Int(rowid),
591 });
592 self.phase = SeqCompactPhase::ScanNext;
596 }
597 SeqCompactPhase::ScanNext => {
598 let r = cursor.next()?;
599 if let IOResult::IO(io) = r {
600 return Ok(IOResult::IO(io));
601 }
602 self.phase = SeqCompactPhase::ScanReadRowid;
606 }
607 }
608 }
609 }
610
611 fn advance_to_next_sequence(&mut self) {
612 self.cursor = None;
613 self.watermark_key = None;
614 self.pending_delete_rowid = None;
615 self.current_idx += 1;
616 self.phase = SeqCompactPhase::SeekWatermark;
617 }
618}
619
620impl<Clock: LogicalClock, A: ConcurrentAllocator> CheckpointStateMachine<Clock, A> {
621 fn pending_sequence_compactions(&self) -> Result<Vec<SeqCompaction>> {
630 let resolve_root = |schema_root: i64| -> Option<i64> {
631 if schema_root > 0 {
632 return Some(schema_root);
633 }
634 if schema_root == 0 {
635 return None;
636 }
637 let table_id = self.mvstore.get_table_id_from_root_page(schema_root);
638 self.mvstore
639 .current_root_page(&table_id)
640 .map(|rp| rp as i64)
641 };
642 let db_id = crate::MAIN_DB_ID;
643 Ok(self
644 .connection
645 .with_schema(db_id, |schema| {
646 crate::without_allocation_faults!(
647 schema
653 .sequences
654 .values()
655 .filter(|seq| !seq.cycle)
656 .filter_map(|seq| {
657 let backing_name =
658 crate::translate::sequence::sequence_backing_table_name(&seq.name);
659 let bt = schema.get_btree_table(&backing_name)?;
660 let backing_root = resolve_root(bt.root_page)?;
661 let table_id = self.mvstore.get_table_id_from_root_page(bt.root_page);
668 Some(SeqCompaction {
669 backing_root,
670 backing_num_cols: bt.columns().len(),
671 table_id,
672 increment_positive: seq.increment_by >= 0,
673 })
674 })
675 .try_collect()
676 )
677 })
678 .expect(ALLOC_ERR_MSG))
679 }
680
681 fn refresh_checkpoint_bounds(&mut self) {
682 let durable_tx_max = self.mvstore.durable_txid_max.load(Ordering::SeqCst);
683 self.durable_txid_max_old = NonZeroU64::new(durable_tx_max);
684 self.durable_txid_max_new = durable_tx_max;
685 }
686
687 fn refresh_schema_metadata(&mut self) {
688 let schema = self.connection.clone_shared_schema(self.database_id);
689 self.index_id_to_index = schema
690 .indexes
691 .values()
692 .flatten()
693 .map(|index| {
694 turso_assert!(index.root_page != 0, "index root_page must be non-zero");
695 (
696 self.mvstore.get_table_id_from_root_page(index.root_page),
697 index.clone(),
698 )
699 })
700 .collect();
701 self.mvcc_meta_table = schema.get_btree_table(MVCC_META_TABLE_NAME).map(|table| {
702 turso_assert!(
703 table.root_page != 0,
704 "mvcc meta table root_page must be non-zero"
705 );
706 (
707 self.mvstore.get_table_id_from_root_page(table.root_page),
708 table.columns().len(),
709 )
710 });
711 self.durable_mvcc_metadata =
712 !self.connection.db.is_in_memory_db() && self.mvcc_meta_table.is_some();
713 }
714
715 pub fn new(
716 pager: Arc<Pager>,
717 mvstore: Arc<MvStore<Clock, A>>,
718 connection: Arc<Connection>,
719 update_transaction_state: bool,
720 sync_mode: SyncMode,
721 database_id: usize,
722 mode: CheckpointMode,
723 ) -> Self {
724 assert!(
725 !matches!(mode, CheckpointMode::Passive { .. })
726 || connection.experimental_mvcc_passive_checkpoint_enabled(),
727 "passive checkpoint mode requires experimental_mvcc_passive_checkpoint"
728 );
729 let mode = match mode {
733 CheckpointMode::Passive { .. } | CheckpointMode::Truncate { .. } => mode,
734 CheckpointMode::Full | CheckpointMode::Restart => CheckpointMode::Truncate {
735 upper_bound_inclusive: None,
736 },
737 };
738 let checkpoint_lock = mvstore.blocking_checkpoint_lock.clone();
739 let schema = connection.clone_shared_schema(database_id);
744 let index_id_to_index = if connection.experimental_mvcc_passive_checkpoint_enabled() {
745 HashMap::default()
746 } else {
747 schema
748 .indexes
749 .values()
750 .flatten()
751 .map(|index| {
752 turso_assert!(index.root_page != 0, "index root_page must be non-zero");
753 (
754 mvstore.get_table_id_from_root_page(index.root_page),
755 index.clone(),
756 )
757 })
758 .collect()
759 };
760
761 let mvcc_meta_table = schema.get_btree_table(MVCC_META_TABLE_NAME).map(|table| {
762 turso_assert!(
763 table.root_page != 0,
764 "mvcc meta table root_page must be non-zero"
765 );
766 (
767 mvstore.get_table_id_from_root_page(table.root_page),
768 table.columns().len(),
769 )
770 });
771 let durable_mvcc_metadata = !connection.db.is_in_memory_db() && mvcc_meta_table.is_some();
772 let durable_tx_max = mvstore.durable_txid_max.load(Ordering::SeqCst);
773 let durable_txid_max_old = NonZeroU64::new(durable_tx_max);
774 #[cfg(any(clt_turso_tests, injected_yields))]
775 let yield_instance_id = connection.next_yield_instance_id();
776 Self {
777 state: CheckpointState::PrepareCheckpoint,
778 lock_states: LockStates {
779 blocking_checkpoint_lock_held: false,
780 pager_read_tx: false,
781 pager_write_tx: false,
782 },
783 pager,
784 durable_txid_max_old,
785 durable_txid_max_new: durable_tx_max,
786 mvstore,
787 connection,
788 database_id,
789 #[cfg(any(clt_turso_tests, injected_yields))]
790 yield_instance_id,
791 checkpoint_lock,
792 write_set: crate::alloc::vec![],
793 write_row_state_machine: None,
794 delete_row_state_machine: None,
795 cursors: HashMap::default(),
796 created_btrees: HashMap::default(),
797 destroyed_tables: HashSet::default(),
798 destroyed_indexes: HashSet::default(),
799 index_write_set: crate::alloc::vec![],
800 index_id_to_index,
801 checkpoint_result: None,
802 update_transaction_state,
803 sync_mode,
804 mode,
805 mvcc_meta_table,
806 durable_mvcc_metadata,
807 staged_checkpoint_header: None,
808 header_staged_for_commit: false,
809 pager_commit_done: false,
810 pending_rootmap_ops: crate::alloc::vec![],
811 pending_alloc_roots: std::collections::HashMap::new(),
812 collect_table_cursor: None,
813 collect_index_tableid_cursor: None,
814 collect_index_key_cursor: None,
815 seq_compact: None,
816 pending_seq_deletes: crate::alloc::vec![],
817 snapshot_ts: u64::MAX,
820 build_local_schema_sm: None,
821 build_local_schema_began_read_tx: false,
822 local_schema: None,
823 owns_checkpoint_in_progress: false,
824 staged_roots: crate::alloc::vec![],
825 }
826 }
827
828 #[cfg(clt_turso_tests)]
829 pub(crate) fn state_for_test(&self) -> CheckpointState {
830 self.state
831 }
832
833 #[cfg(clt_turso_tests)]
834 pub(crate) fn checkpoint_bounds_for_test(&self) -> (Option<u64>, u64) {
835 (
836 self.durable_txid_max_old.map(u64::from),
837 self.durable_txid_max_new,
838 )
839 }
840
841 pub fn cleanup_after_external_io_error(&mut self, err: LimboError) -> Result<()> {
845 let result = self.mvstore.storage.on_checkpoint_end(Err(err));
847
848 self.pending_rootmap_ops.clear();
853 self.pending_alloc_roots.clear();
854
855 if self.lock_states.pager_write_tx {
856 self.pager.rollback_tx(self.connection.as_ref());
857 if self.update_transaction_state {
858 self.connection.set_tx_state(TransactionState::None);
859 }
860 self.lock_states.pager_write_tx = false;
861 self.lock_states.pager_read_tx = false;
862 } else if self.lock_states.pager_read_tx {
863 self.pager.end_read_tx();
864 if self.update_transaction_state {
865 self.connection.set_tx_state(TransactionState::None);
866 }
867 self.lock_states.pager_read_tx = false;
868 }
869
870 self.pager.clear_checkpoint_state();
873 if let Some(wal) = self.pager.wal.as_ref() {
874 wal.abort_checkpoint();
875 }
876
877 if self.lock_states.blocking_checkpoint_lock_held {
879 self.checkpoint_lock.unlock();
880 self.lock_states.blocking_checkpoint_lock_held = false;
881 }
882 if self.owns_checkpoint_in_progress {
884 self.mvstore
885 .checkpoint_in_progress
886 .store(false, Ordering::Release);
887 self.owns_checkpoint_in_progress = false;
888 }
889
890 result
891 }
892
893 fn maybe_get_checkpointable_versions(
895 &self,
896 versions: &[RowVersion],
897 table_id: MVTableId,
898 ) -> smallvec::SmallVec<[RowVersion; 1]> {
899 let mut versions_to_checkpoint: smallvec::SmallVec<[_; 1]> =
900 smallvec::SmallVec::with_capacity(1);
901 let mut exists_in_db_file = false;
902 for version in versions.iter() {
904 let begin_ts = match version.begin() {
911 Some(TxTimestampOrID::Timestamp(e)) => Some(e),
912 Some(TxTimestampOrID::TxID(t)) => {
913 match lookup_tx_state(&self.mvstore.txs, &self.mvstore.finalized_tx_states, t) {
914 Some(crate::mvcc::database::TransactionState::Committed(ts)) => Some(ts),
915 _ => None,
916 }
917 }
918 None => None,
919 };
920 let mut end_ts = match version.end() {
921 Some(TxTimestampOrID::Timestamp(e)) => Some(e),
922 Some(TxTimestampOrID::TxID(t)) => {
923 match lookup_tx_state(&self.mvstore.txs, &self.mvstore.finalized_tx_states, t) {
924 Some(crate::mvcc::database::TransactionState::Committed(ts)) => Some(ts),
925 _ => None,
926 }
927 }
928 None => None,
929 };
930 if begin_ts.is_some_and(|b| b > self.snapshot_ts) {
933 continue;
934 }
935 let future_committed_tombstone = end_ts.is_some_and(|e| e > self.snapshot_ts);
939 if future_committed_tombstone {
940 end_ts = None;
941 }
942 if begin_ts.is_none() && end_ts.is_none() {
943 continue;
946 }
947 if version.btree_resident {
950 exists_in_db_file = true;
951 }
952 if self
957 .durable_txid_max_old
958 .is_some_and(|txid_max_old| begin_ts.is_some_and(|b| b <= u64::from(txid_max_old)))
959 {
960 exists_in_db_file = true;
961 }
962 if self
963 .durable_txid_max_old
964 .is_some_and(|txid_max_old| end_ts.is_some_and(|e| e <= u64::from(txid_max_old)))
965 {
966 exists_in_db_file = false;
967 }
968 let is_uncheckpointed_insert = end_ts.is_none()
973 && self.durable_txid_max_old.is_none_or(|txid_max_old| {
974 begin_ts.is_some_and(|b| b > u64::from(txid_max_old))
975 });
976 let is_delete_and_exists_in_db_file = end_ts.is_some() && exists_in_db_file;
978 let is_schema_delete = table_id == SQLITE_SCHEMA_MVCC_TABLE_ID
986 && !exists_in_db_file
987 && sqlite_schema_btree_identity(version).is_some()
988 && self
989 .durable_txid_max_old
990 .is_none_or(|txid_max_old| end_ts.is_some_and(|e| e > u64::from(txid_max_old)));
991 let should_checkpoint =
992 is_uncheckpointed_insert || is_delete_and_exists_in_db_file || is_schema_delete;
993 if should_checkpoint {
994 let checkpoint_version = if future_committed_tombstone {
996 let mut v = version.clone();
997 v.set_end(None);
998 v
999 } else {
1000 let mut version = version.clone();
1001 version.set_end(end_ts.map(TxTimestampOrID::Timestamp));
1004 version
1005 };
1006 if table_id != SQLITE_SCHEMA_MVCC_TABLE_ID {
1007 if versions_to_checkpoint.is_empty() {
1008 versions_to_checkpoint.push(checkpoint_version)
1009 } else {
1010 versions_to_checkpoint[0] = checkpoint_version
1011 }
1012 continue;
1013 }
1014
1015 if let Some(previous_version) = versions_to_checkpoint.last() {
1016 let should_drop_previous = previous_version.end().is_some()
1017 && !is_schema_metadata_only_rewrite(previous_version, Some(version));
1018 if should_drop_previous {
1019 versions_to_checkpoint.pop();
1020 }
1021 }
1022
1023 versions_to_checkpoint.push(checkpoint_version);
1024 }
1025 }
1026
1027 versions_to_checkpoint
1028 }
1029
1030 fn resolve_dropped_binding(&self, root_page: u64, version: &RowVersion) -> Option<MVTableId> {
1038 let drop_ts = match version.end() {
1039 Some(TxTimestampOrID::Timestamp(t)) => t,
1040 _ => self.snapshot_ts,
1041 };
1042 self.mvstore
1043 .table_id_to_rootpage
1044 .iter()
1045 .find(|entry| {
1046 let e = entry.value();
1047 e.root_page == Some(root_page) && e.begin < drop_ts && drop_ts <= e.end
1048 })
1049 .map(|entry| *entry.key())
1050 }
1051
1052 fn resolve_checkpoint_root(&self, table_id: MVTableId) -> Option<u64> {
1054 self.pending_alloc_roots
1055 .get(&table_id)
1056 .copied()
1057 .or_else(|| self.mvstore.current_root_page(&table_id))
1058 }
1059
1060 fn table_exists_for_snapshot(&self, table_id: MVTableId) -> bool {
1061 if self.pending_alloc_roots.contains_key(&table_id) {
1062 return true;
1063 }
1064 self.mvstore
1065 .table_id_to_rootpage
1066 .get(&table_id)
1067 .is_some_and(|entry| {
1068 let e = entry.value();
1069 e.root_page.is_some() && e.covers(self.snapshot_ts)
1070 })
1071 }
1072
1073 fn ckpt_rootmap_alloc(&mut self, table_id: MVTableId, root_page: u64) {
1076 self.pending_alloc_roots.insert(table_id, root_page);
1077 self.pending_rootmap_ops.push(RootMapOp::Alloc {
1078 id: table_id,
1079 root: root_page,
1080 });
1081 }
1082
1083 fn ckpt_rootmap_retire(&mut self, table_id: MVTableId, end_ts: u64) {
1085 self.pending_rootmap_ops.push(RootMapOp::Retire {
1086 id: table_id,
1087 end_ts,
1088 });
1089 }
1090
1091 fn ckpt_rootmap_remove(&mut self, table_id: MVTableId) {
1093 self.pending_rootmap_ops
1094 .push(RootMapOp::Remove { id: table_id });
1095 }
1096
1097 fn collect_table_rows(&mut self) -> Result<Option<IOCompletions>> {
1106 let bounds: (Bound<RowID>, Bound<RowID>) = match self.collect_table_cursor.clone() {
1111 None => (Bound::Unbounded, Bound::Unbounded),
1112 Some(last) => (Bound::Unbounded, Bound::Excluded(last)),
1113 };
1114 let mut processed = 0;
1115 for entry in self.mvstore.rows.range(bounds).rev() {
1116 let key = entry.key();
1117 tracing::trace!("collecting {key:?}");
1118 self.collect_table_cursor = Some(key.clone());
1119 if self.destroyed_tables.contains(&key.table_id) {
1120 tracing::trace!("skipping {key:?}");
1125 continue;
1126 }
1127
1128 let row_versions = entry.value().read();
1129
1130 for version in self.maybe_get_checkpointable_versions(&row_versions, key.table_id) {
1131 let is_delete = version.end().is_some();
1132
1133 let mut special_write = None;
1134 let mut skip_write = false;
1137
1138 if let Some(schema_identity) = sqlite_schema_btree_identity(&version) {
1139 let root_page = schema_identity.root_page;
1140 match schema_identity.kind {
1141 SqliteSchemaBtreeKind::Index => {
1142 if is_delete {
1144 if root_page < 0 {
1146 let index_id = MVTableId(root_page);
1149 self.destroyed_indexes.insert(index_id);
1150 self.pending_rootmap_ops
1154 .push(RootMapOp::Remove { id: index_id });
1155 skip_write = true;
1156 } else if let Some(index_id) =
1157 self.resolve_dropped_binding(root_page as u64, &version)
1158 {
1159 self.destroyed_indexes.insert(index_id);
1162
1163 let num_columns = self
1167 .index_id_to_index
1168 .get(&index_id)
1169 .map(|index| index.columns.len())
1170 .unwrap_or(0);
1171
1172 special_write = Some(SpecialWrite::BTreeDestroyIndex {
1173 index_id,
1174 root_page: root_page as u64,
1175 num_columns,
1176 });
1177 } else {
1178 skip_write = true;
1182 }
1183 } else if root_page < 0 {
1184 let index_id = MVTableId::from(root_page);
1186 let sqlite_schema_rowid = version.row.id.row_id.to_int_or_panic();
1187 special_write = Some(SpecialWrite::BTreeCreateIndex {
1188 index_id,
1189 sqlite_schema_rowid,
1190 });
1191 } else {
1192 }
1196 }
1197 SqliteSchemaBtreeKind::Table => {
1198 tracing::trace!(
1200 "table schema change with root page {root_page}, is_delete={is_delete}"
1201 );
1202 if is_delete {
1203 if root_page < 0 {
1204 let table_id = MVTableId::from(root_page);
1207 self.destroyed_tables.insert(table_id);
1208 self.pending_rootmap_ops
1211 .push(RootMapOp::Remove { id: table_id });
1212 skip_write = true;
1213 } else if let Some(table_id) =
1214 self.resolve_dropped_binding(root_page as u64, &version)
1215 {
1216 self.destroyed_tables.insert(table_id);
1219
1220 special_write = Some(SpecialWrite::BTreeDestroy {
1222 table_id,
1223 root_page: root_page as u64,
1224 num_columns: version.row.column_count,
1225 });
1226 }
1227 } else if root_page < 0 {
1228 let table_id = MVTableId::from(root_page);
1230 let sqlite_schema_rowid = version.row.id.row_id.to_int_or_panic();
1231 special_write = Some(SpecialWrite::BTreeCreate {
1232 table_id,
1233 sqlite_schema_rowid,
1234 });
1235 } else {
1236 }
1238 }
1239 }
1240 } else if is_delete
1241 && version.row.id.table_id == SQLITE_SCHEMA_MVCC_TABLE_ID
1242 && !version.btree_resident
1243 {
1244 let begin_ts = match &version.begin() {
1248 Some(TxTimestampOrID::Timestamp(ts)) => Some(*ts),
1249 _ => None,
1250 };
1251 let was_checkpointed = self.durable_txid_max_old.is_some_and(|txid_max_old| {
1252 begin_ts.is_some_and(|b| b <= u64::from(txid_max_old))
1253 });
1254 if !was_checkpointed {
1255 skip_write = true;
1256 }
1257 } else if key.table_id != SQLITE_SCHEMA_MVCC_TABLE_ID
1258 && is_delete
1259 && !self.table_exists_for_snapshot(key.table_id)
1260 {
1261 skip_write = true;
1263 }
1264 if !skip_write {
1265 tracing::trace!("adding to write_set {:?}", (&version, &special_write));
1266 with_mvcc_checkpoint_allocation_site!(CheckpointWriteSet, {
1267 self.write_set.try_push((version, special_write))?;
1268 });
1269 }
1270 }
1271 processed += 1;
1272 if processed >= COLLECT_PREEMPTION_THRESHOLD {
1273 return Ok(Some(IOCompletions::Single(Completion::new_yield())));
1274 }
1275 }
1276 self.write_set.sort_by_key(|version| {
1279 (
1280 std::cmp::Reverse(version.0.row.id.table_id),
1282 version.0.row.id.row_id.clone(),
1284 )
1285 });
1286 Ok(None)
1287 }
1288
1289 fn collect_index_rows(&mut self) -> Result<Option<IOCompletions>> {
1297 let outer_bounds: (Bound<MVTableId>, Bound<MVTableId>) =
1298 match self.collect_index_tableid_cursor {
1299 None => (Bound::Unbounded, Bound::Unbounded),
1300 Some(last) if self.collect_index_key_cursor.is_none() => {
1301 (Bound::Excluded(last), Bound::Unbounded)
1302 }
1303 Some(last) => (Bound::Included(last), Bound::Unbounded),
1304 };
1305 let mut processed = 0;
1306 for entry in self.mvstore.index_rows.range(outer_bounds) {
1307 let index_id = *entry.key();
1308
1309 if self.destroyed_indexes.contains(&index_id) {
1311 self.collect_index_tableid_cursor = Some(index_id);
1312 self.collect_index_key_cursor = None;
1313 continue;
1314 }
1315
1316 let index_rows_map = entry.value();
1317 let inner_bounds: (Bound<Arc<SortableIndexKey>>, Bound<Arc<SortableIndexKey>>) =
1318 match self.collect_index_key_cursor.clone() {
1319 None => (Bound::Unbounded, Bound::Unbounded),
1320 Some(last) => (Bound::Excluded(last), Bound::Unbounded),
1321 };
1322 for entry in index_rows_map.range(inner_bounds) {
1323 let versions = entry.value().read();
1324 self.collect_index_tableid_cursor = Some(index_id);
1325 self.collect_index_key_cursor = Some(entry.key().clone());
1326
1327 for version in self.maybe_get_checkpointable_versions(&versions, index_id) {
1328 let is_delete = version.end().is_some();
1329 if is_delete && !self.table_exists_for_snapshot(index_id) {
1330 continue;
1331 }
1332
1333 with_mvcc_checkpoint_allocation_site!(CheckpointIndexWriteSet, {
1336 self.index_write_set
1337 .try_push((index_id, version, is_delete))?;
1338 });
1339 }
1340 processed += 1;
1341 if processed >= COLLECT_PREEMPTION_THRESHOLD {
1342 return Ok(Some(IOCompletions::Single(Completion::new_yield())));
1343 }
1344 }
1345 self.collect_index_tableid_cursor = Some(index_id);
1346 self.collect_index_key_cursor = None;
1347 }
1348 Ok(None)
1349 }
1350
1351 #[cfg(any(clt_turso_tests, debug_assertions))]
1352 fn max_collected_version_timestamp(&self) -> u64 {
1353 fn max_version_timestamp(version: &RowVersion) -> u64 {
1354 [version.begin().as_ref(), version.end().as_ref()]
1355 .into_iter()
1356 .filter_map(|ts| match ts {
1357 Some(TxTimestampOrID::Timestamp(ts)) => Some(*ts),
1358 _ => None,
1359 })
1360 .max()
1361 .unwrap_or_default()
1362 }
1363
1364 let table_max = self
1365 .write_set
1366 .iter()
1367 .map(|(version, _)| max_version_timestamp(version))
1368 .max()
1369 .unwrap_or_default();
1370 let index_max = self
1371 .index_write_set
1372 .iter()
1373 .map(|(_, version, _)| max_version_timestamp(version))
1374 .max()
1375 .unwrap_or_default();
1376 table_max.max(index_max)
1377 }
1378
1379 fn get_current_row_version(
1381 &self,
1382 write_set_index: usize,
1383 ) -> Option<&(RowVersion, Option<SpecialWrite>)> {
1384 self.write_set.get(write_set_index)
1385 }
1386
1387 fn get_current_row_version_mut(
1389 &mut self,
1390 write_set_index: usize,
1391 ) -> Option<&mut (RowVersion, Option<SpecialWrite>)> {
1392 self.write_set.get_mut(write_set_index)
1393 }
1394
1395 fn has_more_rows(&self, write_set_index: usize) -> bool {
1397 write_set_index < self.write_set.len()
1398 }
1399
1400 fn next_requires_seek_after_insert(&self, current_idx: usize) -> bool {
1401 let Some(curr) = self.write_set.get(current_idx) else {
1402 return true;
1403 };
1404 let Some(next) = self.write_set.get(current_idx + 1) else {
1405 return true;
1406 };
1407 if curr.0.row.id.table_id != next.0.row.id.table_id {
1409 return true;
1410 }
1411 if curr.1.is_some() || next.1.is_some() {
1413 return true;
1414 }
1415 let (RowKey::Int(prev_id), RowKey::Int(next_id)) =
1416 (&curr.0.row.id.row_id, &next.0.row.id.row_id)
1417 else {
1418 return true;
1419 };
1420 if next_id.checked_sub(*prev_id) != Some(1) {
1422 return true;
1423 }
1424 false
1425 }
1426
1427 fn fsync_logical_log(&self) -> Result<Completion> {
1429 self.mvstore.storage.sync(self.pager.get_sync_type())
1430 }
1431
1432 fn truncate_logical_log(&self) -> Result<Completion> {
1433 let boundary = if self.mode.should_restart_log() {
1434 u64::MAX
1435 } else {
1436 self.durable_txid_max_new
1437 };
1438 self.mvstore.storage.truncate(boundary)
1439 }
1440
1441 fn checkpoint_wal(&self) -> Result<IOResult<CheckpointResult>> {
1443 let Some(wal) = &self.pager.wal else {
1444 panic!("No WAL to checkpoint");
1445 };
1446 match wal.checkpoint(&self.pager, self.mode)? {
1447 IOResult::Done(result) => Ok(IOResult::Done(result)),
1448 IOResult::IO(io) => Ok(IOResult::IO(io)),
1449 }
1450 }
1451
1452 fn has_unpublished_schema_changes(&self) -> bool {
1453 let schema = self.connection.db.schema.lock();
1454 if !schema.dropped_root_pages.is_empty() {
1455 return true;
1456 }
1457 let owned_negative = |root_page: i64| -> bool {
1463 root_page < 0
1464 && self
1465 .mvstore
1466 .table_id_to_rootpage
1467 .get(&MVTableId::from(root_page))
1468 .and_then(|entry| entry.value().root_page)
1469 .is_some()
1470 };
1471 schema.tables.values().any(|table| {
1472 table
1473 .btree()
1474 .is_some_and(|btree| owned_negative(btree.root_page))
1475 }) || schema
1476 .indexes
1477 .values()
1478 .flatten()
1479 .any(|index| owned_negative(index.root_page))
1480 }
1481
1482 fn has_pending_root_publication(&self) -> bool {
1483 !self.created_btrees.is_empty() || self.has_unpublished_schema_changes()
1484 }
1485
1486 fn publish_checkpointed_schema_roots(&mut self) -> Result<(), TryReserveError> {
1487 if !self.has_pending_root_publication() {
1488 return Ok(());
1489 }
1490
1491 for (sqlite_schema_rowid, (_, row_version)) in self.created_btrees.drain() {
1494 let key = RowID {
1495 table_id: SQLITE_SCHEMA_MVCC_TABLE_ID,
1496 row_id: RowKey::Int(sqlite_schema_rowid),
1497 };
1498 let sqlite_schema_row = self
1499 .mvstore
1500 .rows
1501 .get(&key)
1502 .expect("sqlite_schema row not found");
1503 let mut row_versions = sqlite_schema_row.value().write();
1504 let vid = row_version.id;
1508 if let Some(existing) = row_versions.iter_mut().find(|rv| rv.id == vid) {
1509 *existing = row_version;
1510 } else {
1511 self.mvstore
1512 .insert_version_raw(&mut row_versions, row_version)?;
1513 }
1514 }
1515
1516 if !self.has_unpublished_schema_changes() {
1517 return Ok(());
1518 }
1519
1520 let mut schema_ref = self.connection.db.schema.lock();
1524 let schema = Schema::try_make_mut(&mut schema_ref)?;
1525 for (name, table) in schema.tables.iter_mut() {
1526 #[cfg(not(clt_turso_feature = "conn_raw_api"))]
1527 let _ = name;
1528 let table = Arc::get_mut(table).expect("this should be the only reference");
1529 let Some(btree_table) = table.btree_mut() else {
1530 continue;
1531 };
1532 let btree_table = Arc::make_mut(btree_table);
1533 if btree_table.root_page < 0 {
1534 #[cfg(clt_turso_feature = "conn_raw_api")]
1535 let old_root_page = btree_table.root_page;
1536 let table_id = MVTableId::from(btree_table.root_page);
1537 if let Some(root_page) = self
1540 .mvstore
1541 .table_id_to_rootpage
1542 .get(&table_id)
1543 .and_then(|entry| entry.value().root_page)
1544 {
1545 btree_table.root_page = root_page as i64;
1546 #[cfg(clt_turso_feature = "conn_raw_api")]
1547 {
1548 schema.table_names_by_root_page.remove(&old_root_page);
1549 schema
1550 .table_names_by_root_page
1551 .insert(btree_table.root_page, name.clone());
1552 }
1553 }
1554 }
1555 }
1556 for table_index_list in schema.indexes.values_mut() {
1557 for index in table_index_list.iter_mut() {
1558 if index.root_page < 0 {
1559 let table_id = MVTableId::from(index.root_page);
1560 if let Some(root_page) = self
1562 .mvstore
1563 .table_id_to_rootpage
1564 .get(&table_id)
1565 .and_then(|entry| entry.value().root_page)
1566 {
1567 let index = Arc::make_mut(index);
1568 index.root_page = root_page as i64;
1569 }
1570 }
1571 }
1572 }
1573
1574 schema.dropped_root_pages.clear();
1577 drop(schema_ref);
1578 *self.connection.schema.write() = self.connection.db.clone_schema();
1579 self.connection.bump_prepare_context_generation();
1580 self.mvstore.bump_schema_generation();
1581 Ok(())
1582 }
1583
1584 fn apply_checkpoint_publish_window(
1588 &mut self,
1589 materialized_at: WalPos,
1590 seq_delete_ts: Option<u64>,
1591 ) -> Result<()> {
1592 if let Some(ts) = seq_delete_ts {
1598 for (rowid, num_cols) in
1599 std::mem::replace(&mut self.pending_seq_deletes, crate::alloc::vec![])
1600 {
1601 self.mvstore.seqcompact_commit_delete(rowid, num_cols, ts);
1602 }
1603 }
1604 let ops = std::mem::replace(&mut self.pending_rootmap_ops, crate::alloc::vec![]);
1605 for op in &ops {
1606 match op {
1607 RootMapOp::Retire { id, end_ts } => self.mvstore.retire_rootpage(*id, *end_ts),
1608 RootMapOp::Remove { id } => self.mvstore.remove_table_id_to_rootpage(id),
1609 RootMapOp::Alloc { .. } => {}
1610 }
1611 }
1612 for op in &ops {
1613 if let RootMapOp::Alloc { id, root } = op {
1614 self.mvstore
1615 .record_rootpage_alloc(*id, *root, self.snapshot_ts, WalPos::STAGED);
1616 }
1617 }
1618 self.pending_alloc_roots.clear();
1619 for table_id in std::mem::replace(&mut self.staged_roots, crate::alloc::vec![]) {
1620 self.mvstore
1621 .publish_rootpage_visible(table_id, materialized_at);
1622 }
1623 self.mvstore
1624 .durable_txid_max
1625 .store(self.durable_txid_max_new, Ordering::SeqCst);
1626 self.state = CheckpointState::CheckpointWal;
1627 self.lock_states.pager_read_tx = false;
1628 self.lock_states.pager_write_tx = false;
1629 let header = self.staged_checkpoint_header.take().ok_or_else(|| {
1630 LimboError::InternalError(
1631 "checkpoint header was not staged before pager commit".to_string(),
1632 )
1633 })?;
1634 self.mvstore.global_header.write().replace(header);
1635 crate::without_allocation_faults!(self
1636 .publish_checkpointed_schema_roots()
1637 .expect(crate::alloc::ALLOC_ERR_MSG));
1638 Ok(())
1639 }
1640
1641 fn apply_passive_publish_window_ordered(&mut self, materialized_at: WalPos) -> Result<()> {
1644 let mut publish_err = None;
1645 let sm = self as *mut Self;
1649 self.mvstore.clock.get_timestamp(|ts| {
1650 if let Err(err) =
1653 unsafe { (*sm).apply_checkpoint_publish_window(materialized_at, Some(ts)) }
1654 {
1655 publish_err = Some(err);
1656 }
1657 });
1658 self.mvstore.end_passive_publish_window();
1659 if let Some(err) = publish_err {
1660 return Err(err);
1661 }
1662 Ok(())
1663 }
1664
1665 fn gc_floor_reader_mark(&self) -> WalPos {
1673 let mvcc = self.mvstore.compute_min_reader_mark();
1674 let readers = match self.pager.min_pinned_read_frame() {
1675 Some(frame) => {
1676 let (seq, _) = self.pager.wal_pos();
1677 mvcc.min(WalPos::from_pair((seq, frame)))
1678 }
1679 None => mvcc,
1680 };
1681 readers.min(*self.mvstore.backfill_floor.read())
1685 }
1686
1687 fn gc_checkpointed_table_versions(&mut self) -> Option<IOCompletions> {
1688 let remove_empty_slots = self.lock_states.blocking_checkpoint_lock_held;
1695 let ckpt_max = self.durable_txid_max_new;
1696 let min_reader_mark = self.gc_floor_reader_mark();
1699 let materialized_frame = WalPos::from_pair(self.pager.wal_pos());
1702 let snapshot_ts = self.snapshot_ts;
1703 let CheckpointState::GcTableRows { next_index, lwm } = self.state else {
1704 unreachable!("gc_checkpointed_table_versions runs only in GcTableRows");
1705 };
1706 let mut index = next_index;
1707 let mut processed = 0;
1708 while index < self.write_set.len() {
1709 let current = index;
1710 index += 1;
1711 if current > 0
1712 && self.write_set[current - 1].0.row.id == self.write_set[current].0.row.id
1713 {
1714 continue;
1715 }
1716 let row_id = &self.write_set[current].0.row.id;
1717 if let Some(entry) = self.mvstore.rows.get(row_id) {
1718 let is_now_empty = {
1719 let mut versions = entry.value().write();
1720 self.mvstore.stamp_chain_materialized(
1721 &mut versions,
1722 materialized_frame,
1723 snapshot_ts,
1724 );
1725 MvStore::<Clock, A>::gc_version_chain(
1726 &mut versions,
1727 lwm,
1728 ckpt_max,
1729 self.mvstore.experimental_mvcc_passive_checkpoint,
1730 min_reader_mark,
1731 );
1732 versions.is_empty()
1733 };
1734 if is_now_empty && remove_empty_slots {
1735 self.mvstore.rows.remove(row_id);
1736 }
1737 } else {
1738 assert!(
1742 self.mvcc_meta_table
1743 .is_some_and(|(tid, _)| tid == row_id.table_id),
1744 "row {row_id:?} missing from MVCC store but is not an MVCC metadata table row"
1745 );
1746 }
1747 processed += 1;
1748 if processed >= COLLECT_PREEMPTION_THRESHOLD {
1749 break;
1750 }
1751 }
1752 if index < self.write_set.len() {
1753 let CheckpointState::GcTableRows { next_index, .. } = &mut self.state else {
1754 unreachable!("gc_checkpointed_table_versions runs only in GcTableRows");
1755 };
1756 *next_index = index;
1757
1758 Some(IOCompletions::Single(Completion::new_yield()))
1759 } else {
1760 None
1761 }
1762 }
1763
1764 fn gc_checkpointed_index_versions(&mut self) -> Option<IOCompletions> {
1765 let remove_empty_slots = self.lock_states.blocking_checkpoint_lock_held;
1768 let ckpt_max = self.durable_txid_max_new;
1769 let min_reader_mark = self.gc_floor_reader_mark();
1770 let materialized_frame = WalPos::from_pair(self.pager.wal_pos());
1771 let snapshot_ts = self.snapshot_ts;
1772 let CheckpointState::GcIndexRows { next_index, lwm } = self.state else {
1773 unreachable!("gc_checkpointed_index_versions runs only in GcIndexRows");
1774 };
1775 let mut index = next_index;
1776 let mut processed = 0;
1777 while index < self.index_write_set.len() {
1778 let current = index;
1779 index += 1;
1780 {
1781 let (index_id, row_version, _is_delete) = &self.index_write_set[current];
1782 let index_id = *index_id;
1783 let RowKey::Record(sortable_key) = &row_version.row.id.row_id else {
1784 unreachable!("index row versions always have Record keys");
1785 };
1786 let outer_entry = self
1787 .mvstore
1788 .index_rows
1789 .get(&index_id)
1790 .expect("index_id from write set must exist in index_rows");
1791 let inner_map = outer_entry.value();
1792 let is_now_empty = {
1793 let inner_entry = inner_map
1794 .get(sortable_key)
1795 .expect("index row from write set must exist in inner map");
1796 let mut versions = inner_entry.value().write();
1797 self.mvstore.stamp_chain_materialized(
1798 &mut versions,
1799 materialized_frame,
1800 snapshot_ts,
1801 );
1802 MvStore::<Clock, A>::gc_version_chain(
1803 &mut versions,
1804 lwm,
1805 ckpt_max,
1806 self.mvstore.experimental_mvcc_passive_checkpoint,
1807 min_reader_mark,
1808 );
1809 versions.is_empty()
1810 };
1811 if is_now_empty && remove_empty_slots {
1812 inner_map.remove(sortable_key);
1813 }
1814 }
1815 processed += 1;
1816 if processed >= COLLECT_PREEMPTION_THRESHOLD {
1817 break;
1818 }
1819 }
1820 if index < self.index_write_set.len() {
1821 let CheckpointState::GcIndexRows { next_index, .. } = &mut self.state else {
1822 unreachable!("gc_checkpointed_index_versions runs only in GcIndexRows");
1823 };
1824 *next_index = index;
1825 Some(IOCompletions::Single(Completion::new_yield()))
1826 } else {
1827 None
1828 }
1829 }
1830
1831 fn maybe_stage_mvcc_metadata_write(&mut self) -> Result<()> {
1837 if !self.durable_mvcc_metadata {
1838 return Ok(());
1839 }
1840 let old = self.durable_txid_max_old.map(u64::from).unwrap_or_default();
1841 let new = self.durable_txid_max_new;
1842 if new <= old {
1843 return Ok(());
1844 }
1845
1846 let (table_id, num_columns) = self.mvcc_meta_table.ok_or_else(|| {
1847 LimboError::Corrupt(format!(
1848 "Missing required internal metadata table {MVCC_META_TABLE_NAME}"
1849 ))
1850 })?;
1851 let new_i64 = i64::try_from(new).map_err(|_| {
1852 LimboError::Corrupt(format!("MVCC checkpoint timestamp does not fit i64: {new}"))
1853 })?;
1854 let record = with_mvcc_checkpoint_allocation_site!(
1855 CheckpointMetadataPayload,
1856 ImmutableRecord::from_values(
1857 &[
1858 Value::build_text(MVCC_META_KEY_PERSISTENT_TX_TS_MAX),
1859 Value::from_i64(new_i64),
1860 ],
1861 2,
1862 )?
1863 );
1864 let row = with_mvcc_checkpoint_allocation_site!(
1865 CheckpointMetadataPayload,
1866 Row::new_table_row_in(
1867 RowID::new(table_id, RowKey::Int(1)),
1868 record.get_payload(),
1869 num_columns,
1870 self.mvstore.allocator(),
1871 )?
1872 );
1873 with_mvcc_checkpoint_allocation_site!(CheckpointWriteSet, {
1874 self.write_set.try_push((
1875 RowVersion {
1876 id: 0,
1877 begin: crate::mvcc::database::PackedTs::pack(Some(TxTimestampOrID::Timestamp(
1878 new,
1879 ))),
1880 end: crate::mvcc::database::PackedTs::pack(None),
1881 row,
1882 btree_resident: true,
1883 materialized_at: crate::mvcc::database::WalPos::ORIGIN,
1884 },
1885 None,
1886 ))?;
1887 });
1888 Ok(())
1889 }
1890
1891 fn step_inner(&mut self, _context: &()) -> Result<TransitionResult<CheckpointResult>> {
1892 match &self.state {
1893 CheckpointState::PrepareCheckpoint => {
1894 let passive = self
1895 .connection
1896 .experimental_mvcc_passive_checkpoint_enabled();
1897 if passive {
1898 if self
1905 .mvstore
1906 .checkpoint_in_progress
1907 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1908 .is_err()
1909 {
1910 self.state = CheckpointState::Finalize;
1912 return Ok(TransitionResult::Done(CheckpointResult::default()));
1913 }
1914 self.owns_checkpoint_in_progress = true;
1915 }
1916
1917 if passive {
1918 self.snapshot_ts = self.mvstore.checkpoint_snapshot_ts();
1919 self.refresh_checkpoint_bounds();
1922 self.state = CheckpointState::BuildLocalSchemaView;
1923 } else {
1924 self.state = CheckpointState::AcquireLock;
1925 }
1926 Ok(TransitionResult::Continue)
1927 }
1928 CheckpointState::AcquireLock => {
1929 inject_transition_yield!(self, CheckpointYieldPoint::BeforeAcquireLock);
1930
1931 tracing::debug!("Acquiring blocking checkpoint lock");
1932 if !self.lock_states.blocking_checkpoint_lock_held {
1933 let locked = self.checkpoint_lock.write();
1934 if !locked {
1935 return Err(crate::LimboError::Busy);
1936 }
1937 self.lock_states.blocking_checkpoint_lock_held = true;
1938 }
1939
1940 self.snapshot_ts = self.mvstore.checkpoint_snapshot_ts();
1943 self.refresh_checkpoint_bounds();
1948 self.refresh_schema_metadata();
1949 self.state = CheckpointState::CollectTableRows;
1950 Ok(TransitionResult::Continue)
1951 }
1952 CheckpointState::BuildLocalSchemaView => {
1953 if self.build_local_schema_sm.is_none() {
1954 let began = !self
1955 .pager
1956 .wal
1957 .as_ref()
1958 .is_some_and(|wal| wal.holds_read_lock());
1959 if began {
1960 self.pager.begin_read_tx()?;
1961 }
1962 self.build_local_schema_began_read_tx = began;
1963 let cursor = BTreeCursor::new_table(
1964 self.pager.clone(),
1965 SQLITE_SCHEMA_ROOT_PAGE,
1966 SQLITE_SCHEMA_COLUMN_COUNT,
1967 );
1968 self.build_local_schema_sm =
1969 Some(StateMachine::new(BuildLocalSchemaViewStateMachine::new(
1970 cursor,
1971 self.mvstore.clone(),
1972 self.connection.clone(),
1973 self.snapshot_ts,
1974 )));
1975 }
1976 let sm = self
1977 .build_local_schema_sm
1978 .as_mut()
1979 .expect("build_local_schema_sm just set");
1980 match sm.step(&())? {
1981 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
1982 IOResult::Done(schema) => {
1983 self.local_schema = Some(schema);
1984 let local = self
1985 .local_schema
1986 .as_ref()
1987 .expect("local_schema just set")
1988 .clone();
1989 self.index_id_to_index = local
1997 .indexes
1998 .values()
1999 .flatten()
2000 .filter_map(|index| {
2001 self.mvstore
2002 .try_get_table_id_from_root_page_at(
2003 index.root_page,
2004 self.snapshot_ts,
2005 )
2006 .map(|id| (id, index.clone()))
2007 })
2008 .collect();
2009 self.build_local_schema_sm = None;
2010 if self.build_local_schema_began_read_tx {
2011 self.pager.end_read_tx();
2012 self.build_local_schema_began_read_tx = false;
2013 }
2014 self.state = CheckpointState::CollectTableRows;
2015 Ok(TransitionResult::Continue)
2016 }
2017 }
2018 }
2019 CheckpointState::CollectTableRows => {
2020 if let Some(io) = self.collect_table_rows()? {
2021 return Ok(TransitionResult::Io(io));
2022 }
2023 tracing::debug!("Collected {} committed versions", self.write_set.len());
2024 self.state = CheckpointState::CollectIndexRows;
2025 inject_transition_yield!(self, CheckpointYieldPoint::AfterCollectTableRows);
2026 Ok(TransitionResult::Continue)
2027 }
2028 CheckpointState::CollectIndexRows => {
2029 if let Some(io) = self.collect_index_rows()? {
2030 return Ok(TransitionResult::Io(io));
2031 }
2032 tracing::debug!("Collected {} index row changes", self.index_write_set.len());
2033
2034 let passive = self
2035 .connection
2036 .experimental_mvcc_passive_checkpoint_enabled();
2037 if passive {
2038 inject_transition_yield!(self, CheckpointYieldPoint::BeforeAcquireLock);
2039 }
2043
2044 let durable_old = self.durable_txid_max_old.map(u64::from).unwrap_or_default();
2045 #[cfg(any(clt_turso_tests, debug_assertions))]
2046 {
2047 let collected_max = self.max_collected_version_timestamp();
2048 turso_assert!(
2049 self.snapshot_ts >= collected_max,
2050 "MVCC checkpoint collected version timestamp above snapshot",
2051 { "collected_max": collected_max, "snapshot_ts": self.snapshot_ts }
2052 );
2053 }
2054 self.durable_txid_max_new = durable_old.max(self.snapshot_ts);
2055 self.maybe_stage_mvcc_metadata_write()?;
2056
2057 self.mvstore.storage.on_checkpoint_start()?;
2058
2059 if self.write_set.is_empty() && self.index_write_set.is_empty() {
2060 self.state = CheckpointState::CheckpointWal;
2062 } else {
2063 self.state = CheckpointState::BeginPagerTxn;
2064 }
2065 Ok(TransitionResult::Continue)
2066 }
2067 CheckpointState::BeginPagerTxn => {
2068 tracing::debug!("Beginning pager transaction");
2069 let read_tx_active = self
2071 .pager
2072 .wal
2073 .as_ref()
2074 .is_some_and(|wal| wal.holds_read_lock());
2075 if !read_tx_active {
2076 self.pager.begin_read_tx()?;
2077 self.lock_states.pager_read_tx = true;
2078 }
2079
2080 self.pager
2081 .io
2082 .block(|| self.pager.begin_write_tx(WalAutoActions::all_enabled()))?;
2083 if self.update_transaction_state {
2084 self.connection.set_tx_state(TransactionState::Write {
2085 schema_did_change: false,
2086 }); }
2088 self.lock_states.pager_write_tx = true;
2089 self.state = CheckpointState::WriteRow {
2090 write_set_index: 0,
2091 requires_seek: true,
2092 };
2093 Ok(TransitionResult::Continue)
2094 }
2095
2096 CheckpointState::WriteRow {
2097 write_set_index,
2098 requires_seek,
2099 } => {
2100 let write_set_index = *write_set_index;
2101 let requires_seek = *requires_seek;
2102
2103 if !self.has_more_rows(write_set_index) {
2104 if self.index_write_set.is_empty() {
2106 self.state = CheckpointState::CompactSequences;
2109 } else {
2110 self.state = CheckpointState::WriteIndexRow {
2112 index_write_set_index: 0,
2113 requires_seek: true,
2114 };
2115 }
2116 return Ok(TransitionResult::Continue);
2117 }
2118
2119 let (num_columns, table_id, special_write, drop_ts) = {
2120 let (row_version, special_write) = self
2121 .get_current_row_version(write_set_index)
2122 .ok_or_else(|| {
2123 LimboError::InternalError(
2124 "row version not found in write set".to_string(),
2125 )
2126 })?;
2127 tracing::trace!("checkpointing row {row_version:?} ");
2128 let drop_ts = match row_version.end() {
2132 Some(TxTimestampOrID::Timestamp(ts)) => Some(ts),
2133 _ => None,
2134 };
2135 (
2136 row_version.row.column_count,
2137 row_version.row.id.table_id,
2138 *special_write,
2139 drop_ts,
2140 )
2141 };
2142 tracing::debug!(
2143 "WriteRow: num_columns={num_columns}, table_id={table_id:?}, special_write={special_write:?}"
2144 );
2145
2146 if let Some(special_write) = special_write {
2148 match special_write {
2149 SpecialWrite::BTreeCreate { table_id, .. } => {
2150 let created_root_page: u32 = self.pager.io.block(|| {
2151 self.pager.btree_create(&CreateBTreeFlags::new_table())
2152 })?;
2153 self.ckpt_rootmap_alloc(table_id, created_root_page as u64);
2159 self.staged_roots.push(table_id);
2160 }
2161 SpecialWrite::BTreeDestroy {
2162 table_id,
2163 root_page,
2164 num_columns,
2165 } => {
2166 let known_root_page = self
2167 .mvstore
2168 .current_root_page(&table_id)
2169 .expect("Table ID does not have a root page");
2170 turso_assert_eq!(
2171 known_root_page,
2172 root_page,
2173 "checkpoint root page mismatch for BTreeDestroy",
2174 { "known_root_page": known_root_page, "schema_root_page": root_page }
2175 );
2176 let cursor = if let Some(cursor) = self.cursors.get(&known_root_page) {
2177 cursor.clone()
2178 } else {
2179 let cursor = BTreeCursor::new_table(
2180 self.pager.clone(),
2181 known_root_page as i64,
2182 num_columns,
2183 );
2184 let cursor = Arc::new(RwLock::new(cursor));
2185 self.cursors.insert(root_page, cursor.clone());
2186 cursor
2187 };
2188 self.pager.io.block(|| cursor.write().btree_destroy())?;
2189 self.cursors.remove(&root_page);
2191 self.destroyed_tables.insert(table_id);
2192 if let Some(drop_ts) = drop_ts {
2197 self.ckpt_rootmap_retire(table_id, drop_ts);
2198 } else {
2199 self.ckpt_rootmap_remove(table_id);
2200 }
2201 }
2202 SpecialWrite::BTreeCreateIndex { index_id, .. } => {
2203 let created_root_page: u32 = self.pager.io.block(|| {
2204 self.pager.btree_create(&CreateBTreeFlags::new_index())
2205 })?;
2206 self.ckpt_rootmap_alloc(index_id, created_root_page as u64);
2209 self.staged_roots.push(index_id);
2210 turso_assert!(
2212 self.index_id_to_index.contains_key(&index_id),
2213 "checkpoint index struct missing before BTreeCreateIndex",
2214 { "index_id": i64::from(index_id) }
2215 );
2216 }
2217 SpecialWrite::BTreeDestroyIndex {
2218 index_id,
2219 root_page,
2220 num_columns,
2221 } => {
2222 let known_root_page = self
2223 .mvstore
2224 .current_root_page(&index_id)
2225 .expect("Index ID does not have a root page");
2226 turso_assert_eq!(
2227 known_root_page,
2228 root_page,
2229 "checkpoint root page mismatch for BTreeDestroyIndex",
2230 { "known_root_page": known_root_page, "schema_root_page": root_page }
2231 );
2232
2233 let cursor = if let Some(cursor) = self.cursors.get(&known_root_page) {
2234 cursor.clone()
2235 } else if let Some(index) = self.index_id_to_index.get(&index_id) {
2236 let cursor = BTreeCursor::new_index(
2237 self.pager.clone(),
2238 known_root_page as i64,
2239 index.as_ref(),
2240 num_columns,
2241 )?;
2242 let cursor = Arc::new(RwLock::new(cursor));
2243 self.cursors.insert(root_page, cursor.clone());
2244 cursor
2245 } else {
2246 Arc::new(RwLock::new(BTreeCursor::new_table(
2249 self.pager.clone(),
2250 known_root_page as i64,
2251 num_columns,
2252 )))
2253 };
2254 self.pager.io.block(|| cursor.write().btree_destroy())?;
2255 self.cursors.remove(&root_page);
2257 self.destroyed_indexes.insert(index_id);
2258 if let Some(drop_ts) = drop_ts {
2262 self.ckpt_rootmap_retire(index_id, drop_ts);
2263 } else {
2264 self.ckpt_rootmap_remove(index_id);
2265 }
2266 }
2267 }
2268 }
2269
2270 if self.destroyed_tables.contains(&table_id) {
2271 self.state = CheckpointState::WriteRow {
2273 write_set_index: write_set_index + 1,
2274 requires_seek: true,
2275 };
2276 return Ok(TransitionResult::Continue);
2277 }
2278
2279 let is_delete = self
2280 .get_current_row_version(write_set_index)
2281 .is_some_and(|(v, _)| v.end().is_some());
2282 if is_delete && !self.table_exists_for_snapshot(table_id) {
2283 self.state = CheckpointState::WriteRow {
2284 write_set_index: write_set_index + 1,
2285 requires_seek: true,
2286 };
2287 return Ok(TransitionResult::Continue);
2288 }
2289
2290 let root_page = self.resolve_checkpoint_root(table_id).unwrap_or_else(|| {
2291 panic!(
2292 "Table ID does not have a root page: {table_id}, row_version: {:?}",
2293 self.get_current_row_version(write_set_index)
2294 .expect("row version should exist")
2295 )
2296 });
2297
2298 tracing::debug!("WriteRow: resolved root page: root_page={root_page}");
2299
2300 if let Some(SpecialWrite::BTreeCreate {
2303 table_id,
2304 sqlite_schema_rowid,
2305 }) = special_write
2306 {
2307 let root_page = self
2308 .resolve_checkpoint_root(table_id)
2309 .expect("Table ID does not have a root page");
2310 let row_version = {
2311 let alloc = self.mvstore.allocator();
2312 let (row_version, _) = self
2313 .get_current_row_version_mut(write_set_index)
2314 .ok_or_else(|| {
2315 LimboError::InternalError(
2316 "row version not found in write set".to_string(),
2317 )
2318 })?;
2319 let record = ImmutableRecordRef::from_bin_record(row_version.row.payload());
2320
2321 let mut values = record.get_values_owned()?;
2322 values[3] = Value::from_i64(root_page as i64);
2323 let record = ImmutableRecord::from_values(&values, values.len())?;
2324 row_version.row.data = Some(crate::without_allocation_faults!(
2329 crate::alloc::try_arc_slice_from_slice_in(record.get_payload(), alloc)?
2330 ));
2331 row_version.clone()
2332 };
2333 self.created_btrees
2334 .insert(sqlite_schema_rowid, (table_id, row_version));
2335 } else if let Some(SpecialWrite::BTreeCreateIndex {
2336 index_id,
2337 sqlite_schema_rowid,
2338 }) = special_write
2339 {
2340 let root_page = self
2342 .resolve_checkpoint_root(index_id)
2343 .expect("Index ID does not have a root page");
2344 let row_version = {
2345 let alloc = self.mvstore.allocator();
2346 let (row_version, _) = self
2347 .get_current_row_version_mut(write_set_index)
2348 .ok_or_else(|| {
2349 LimboError::InternalError(
2350 "row version not found in write set".to_string(),
2351 )
2352 })?;
2353 let record = ImmutableRecordRef::from_bin_record(row_version.row.payload());
2354 let mut values = record.get_values_owned()?;
2355 values[3] = Value::from_i64(root_page as i64);
2356 let record = ImmutableRecord::from_values(&values, values.len())?;
2357 row_version.row.data = Some(crate::without_allocation_faults!(
2362 crate::alloc::try_arc_slice_from_slice_in(record.get_payload(), alloc)?
2363 ));
2364 row_version.clone()
2365 };
2366
2367 self.created_btrees
2368 .insert(sqlite_schema_rowid, (index_id, row_version));
2369 }
2370
2371 let cursor = if let Some(cursor) = self.cursors.get(&root_page) {
2373 cursor.clone()
2374 } else {
2375 let cursor =
2376 BTreeCursor::new_table(self.pager.clone(), root_page as i64, num_columns);
2377 let cursor = Arc::new(RwLock::new(cursor));
2378 self.cursors.insert(root_page, cursor.clone());
2379 cursor
2380 };
2381
2382 let (row_version, _) =
2383 self.get_current_row_version(write_set_index)
2384 .ok_or_else(|| {
2385 LimboError::InternalError(
2386 "row version not found in write set".to_string(),
2387 )
2388 })?;
2389
2390 if row_version.end().is_some() {
2392 if self
2396 .created_btrees
2397 .values()
2398 .any(|(table_id, _)| *table_id == row_version.row.id.table_id)
2399 {
2400 self.state = CheckpointState::WriteRow {
2401 write_set_index: write_set_index + 1,
2402 requires_seek: true,
2403 };
2404 return Ok(TransitionResult::Continue);
2405 }
2406 let state_machine = self
2407 .mvstore
2408 .delete_row_from_pager(row_version.row.id.clone(), cursor)?;
2409 self.delete_row_state_machine = Some(state_machine);
2410 self.state = CheckpointState::DeleteRowStateMachine { write_set_index };
2411 } else {
2412 let state_machine =
2414 self.mvstore
2415 .write_row_to_pager(&row_version.row, cursor, requires_seek)?;
2416 self.write_row_state_machine = Some(state_machine);
2417 self.state = CheckpointState::WriteRowStateMachine { write_set_index };
2418 }
2419
2420 Ok(TransitionResult::Continue)
2421 }
2422
2423 CheckpointState::WriteRowStateMachine { write_set_index } => {
2424 let write_set_index = *write_set_index;
2425 let write_row_state_machine =
2426 self.write_row_state_machine.as_mut().ok_or_else(|| {
2427 LimboError::InternalError(
2428 "write_row_state_machine not initialized".to_string(),
2429 )
2430 })?;
2431
2432 match write_row_state_machine.step(&())? {
2433 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2434 IOResult::Done(_) => {
2435 let requires_seek = self.next_requires_seek_after_insert(write_set_index);
2436 self.state = CheckpointState::WriteRow {
2437 write_set_index: write_set_index + 1,
2438 requires_seek,
2439 };
2440 Ok(TransitionResult::Continue)
2441 }
2442 }
2443 }
2444
2445 CheckpointState::DeleteRowStateMachine { write_set_index } => {
2446 let write_set_index = *write_set_index;
2447 let delete_row_state_machine =
2448 self.delete_row_state_machine.as_mut().ok_or_else(|| {
2449 LimboError::InternalError(
2450 "delete_row_state_machine not initialized".to_string(),
2451 )
2452 })?;
2453
2454 match delete_row_state_machine.step(&())? {
2455 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2456 IOResult::Done(_) => {
2457 self.state = CheckpointState::WriteRow {
2458 write_set_index: write_set_index + 1,
2459 requires_seek: true,
2460 };
2461 Ok(TransitionResult::Continue)
2462 }
2463 }
2464 }
2465
2466 CheckpointState::WriteIndexRow {
2467 index_write_set_index,
2468 requires_seek,
2469 } => {
2470 let index_write_set_index = *index_write_set_index;
2471 let requires_seek = *requires_seek;
2472
2473 if index_write_set_index >= self.index_write_set.len() {
2474 self.state = CheckpointState::CompactSequences;
2477 return Ok(TransitionResult::Continue);
2478 }
2479
2480 let (index_id, row_version, is_delete) =
2481 &self.index_write_set[index_write_set_index];
2482
2483 if self.destroyed_indexes.contains(index_id) {
2485 self.state = CheckpointState::WriteIndexRow {
2486 index_write_set_index: index_write_set_index + 1,
2487 requires_seek: true,
2488 };
2489 return Ok(TransitionResult::Continue);
2490 }
2491
2492 let Some(index) = self.index_id_to_index.get(index_id) else {
2500 self.state = CheckpointState::WriteIndexRow {
2501 index_write_set_index: index_write_set_index + 1,
2502 requires_seek: true,
2503 };
2504 return Ok(TransitionResult::Continue);
2505 };
2506
2507 if *is_delete && !self.table_exists_for_snapshot(*index_id) {
2508 self.state = CheckpointState::WriteIndexRow {
2509 index_write_set_index: index_write_set_index + 1,
2510 requires_seek: true,
2511 };
2512 return Ok(TransitionResult::Continue);
2513 }
2514
2515 let root_page = self
2517 .resolve_checkpoint_root(*index_id)
2518 .unwrap_or_else(|| panic!("Index ID {index_id} does not have a root page"));
2519
2520 let cursor = if let Some(cursor) = self.cursors.get(&root_page) {
2522 cursor.clone()
2523 } else {
2524 let cursor = BTreeCursor::new_index(
2525 self.pager.clone(),
2526 root_page as i64,
2527 index.as_ref(),
2528 index.columns.len(),
2529 )?;
2530 let cursor = Arc::new(RwLock::new(cursor));
2531 self.cursors.insert(root_page, cursor.clone());
2532 cursor
2533 };
2534
2535 if *is_delete {
2537 if self
2540 .created_btrees
2541 .values()
2542 .any(|(table_id, _)| *table_id == row_version.row.id.table_id)
2543 {
2544 self.state = CheckpointState::WriteIndexRow {
2545 index_write_set_index: index_write_set_index + 1,
2546 requires_seek: true,
2547 };
2548 return Ok(TransitionResult::Continue);
2549 }
2550 let state_machine = self
2551 .mvstore
2552 .delete_row_from_pager(row_version.row.id.clone(), cursor)?;
2553 self.delete_row_state_machine = Some(state_machine);
2554 self.state = CheckpointState::DeleteIndexRowStateMachine {
2555 index_write_set_index,
2556 };
2557 } else {
2558 let state_machine =
2560 self.mvstore
2561 .write_row_to_pager(&row_version.row, cursor, requires_seek)?;
2562 self.write_row_state_machine = Some(state_machine);
2563 self.state = CheckpointState::WriteIndexRowStateMachine {
2564 index_write_set_index,
2565 };
2566 }
2567
2568 Ok(TransitionResult::Continue)
2569 }
2570
2571 CheckpointState::WriteIndexRowStateMachine {
2572 index_write_set_index,
2573 } => {
2574 let index_write_set_index = *index_write_set_index;
2575 let write_row_state_machine =
2576 self.write_row_state_machine.as_mut().ok_or_else(|| {
2577 LimboError::InternalError(
2578 "write_row_state_machine not initialized".to_string(),
2579 )
2580 })?;
2581
2582 match write_row_state_machine.step(&())? {
2583 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2584 IOResult::Done(_) => {
2585 self.state = CheckpointState::WriteIndexRow {
2586 index_write_set_index: index_write_set_index + 1,
2587 requires_seek: true,
2588 };
2589 Ok(TransitionResult::Continue)
2590 }
2591 }
2592 }
2593
2594 CheckpointState::DeleteIndexRowStateMachine {
2595 index_write_set_index,
2596 } => {
2597 let index_write_set_index = *index_write_set_index;
2598 let delete_row_state_machine =
2599 self.delete_row_state_machine.as_mut().ok_or_else(|| {
2600 LimboError::InternalError(
2601 "delete_row_state_machine not initialized".to_string(),
2602 )
2603 })?;
2604
2605 match delete_row_state_machine.step(&())? {
2606 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2607 IOResult::Done(_) => {
2608 self.state = CheckpointState::WriteIndexRow {
2609 index_write_set_index: index_write_set_index + 1,
2610 requires_seek: true,
2611 };
2612 Ok(TransitionResult::Continue)
2613 }
2614 }
2615 }
2616
2617 CheckpointState::CompactSequences => {
2618 if self.seq_compact.is_none() {
2619 let pending = self.pending_sequence_compactions()?;
2620 if pending.is_empty() {
2621 self.state = CheckpointState::CommitPagerTxn;
2622 return Ok(TransitionResult::Continue);
2623 }
2624 self.seq_compact = Some(SeqCompactDriver {
2625 pending,
2626 current_idx: 0,
2627 cursor: None,
2628 phase: SeqCompactPhase::SeekWatermark,
2629 watermark_key: None,
2630 pending_delete_rowid: None,
2631 pager: self.pager.clone(),
2632 mvstore: self.mvstore.clone(),
2633 passive: matches!(self.mode, CheckpointMode::Passive { .. }),
2634 compacted: crate::alloc::vec![],
2635 });
2636 }
2637 let driver = self.seq_compact.as_mut().expect("seq_compact set above");
2638 match driver.step()? {
2639 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2640 IOResult::Done(()) => {
2641 let driver = self.seq_compact.take().expect("seq_compact set above");
2644 self.pending_seq_deletes = driver.compacted;
2645 self.state = CheckpointState::CommitPagerTxn;
2646 Ok(TransitionResult::Continue)
2647 }
2648 }
2649 }
2650 CheckpointState::CommitPagerTxn => {
2651 let passive = matches!(self.mode, CheckpointMode::Passive { .. });
2652 let passive_auto_publish_retry = passive && !self.update_transaction_state;
2653 let lock_before_commit = !passive;
2655 if lock_before_commit && !self.lock_states.blocking_checkpoint_lock_held {
2656 if !self.checkpoint_lock.write() {
2657 return Err(crate::LimboError::Busy);
2658 }
2659 self.lock_states.blocking_checkpoint_lock_held = true;
2660 }
2661 if !self.pager_commit_done {
2662 if !self.header_staged_for_commit {
2663 let mut checkpoint_header =
2664 *self.mvstore.global_header.read().as_ref().ok_or_else(|| {
2665 LimboError::InternalError(
2666 "global_header not initialized during checkpoint".to_string(),
2667 )
2668 })?;
2669 checkpoint_header.schema_cookie =
2670 self.connection.db.schema.lock().schema_version.into();
2671 let staged_header = self.pager.io.block(|| {
2672 self.pager.with_header_mut(|header| {
2673 header.schema_cookie = checkpoint_header.schema_cookie;
2677 header.user_version = checkpoint_header.user_version;
2678 header.application_id = checkpoint_header.application_id;
2679 header.vacuum_mode_largest_root_page =
2680 checkpoint_header.vacuum_mode_largest_root_page;
2681 header.incremental_vacuum_enabled =
2682 checkpoint_header.incremental_vacuum_enabled;
2683 *header
2684 })
2685 })?;
2686 self.staged_checkpoint_header = Some(staged_header);
2687 self.header_staged_for_commit = true;
2688 }
2689 tracing::debug!("Committing pager transaction");
2692 match self
2693 .pager
2694 .commit_tx(&self.connection, self.update_transaction_state)?
2695 {
2696 IOResult::Done(_) => {
2697 self.pager_commit_done = true;
2698 }
2699 IOResult::IO(io) => return Ok(TransitionResult::Io(io)),
2700 }
2701 }
2702 if passive {
2703 if !self.mvstore.try_begin_passive_publish_window() {
2704 if passive_auto_publish_retry {
2705 tracing::debug!(
2706 "passive checkpoint publish contended; yielding for retry"
2707 );
2708 return Ok(TransitionResult::Io(IOCompletions::Single(
2709 Completion::new_yield(),
2710 )));
2711 }
2712 return Err(crate::LimboError::Busy);
2713 }
2714 let materialized_at = WalPos::from_pair(self.pager.wal_pos());
2715 self.apply_passive_publish_window_ordered(materialized_at)?;
2716 } else if !self.lock_states.blocking_checkpoint_lock_held {
2717 if !self.checkpoint_lock.write() {
2718 return Err(crate::LimboError::Busy);
2719 }
2720 self.lock_states.blocking_checkpoint_lock_held = true;
2721 let materialized_at = WalPos::from_pair(self.pager.wal_pos());
2722 self.apply_checkpoint_publish_window(materialized_at, None)?;
2725 } else {
2726 let materialized_at = WalPos::from_pair(self.pager.wal_pos());
2727 self.apply_checkpoint_publish_window(materialized_at, None)?;
2728 }
2729 inject_transition_failure!(
2730 self,
2731 CheckpointYieldPoint::AfterDurableBoundaryAdvanced
2732 );
2733 inject_transition_yield!(self, CheckpointYieldPoint::AfterDurableBoundaryAdvanced);
2734 Ok(TransitionResult::Continue)
2735 }
2736
2737 CheckpointState::TruncateLogicalLog => {
2738 tracing::debug!("Truncating logical log file");
2739 let c = self.truncate_logical_log()?;
2740 self.state = CheckpointState::FsyncLogicalLog;
2741 if c.succeeded() {
2743 if self.mode.should_restart_log() {
2744 turso_assert!(
2745 self.mvstore.storage.logical_log_offset() == 0,
2746 "TRUNCATE checkpoint must reset logical log offset to 0"
2747 );
2748 turso_assert!(
2749 self.mvstore
2750 .get_logical_log_file()
2751 .size()
2752 .expect("logical log file size should be readable after truncate")
2753 == 0,
2754 "TRUNCATE checkpoint must zero the logical log file"
2755 );
2756 }
2757 Ok(TransitionResult::Continue)
2758 } else {
2759 Ok(TransitionResult::Io(IOCompletions::Single(c)))
2760 }
2761 }
2762
2763 CheckpointState::FsyncLogicalLog => {
2764 if self.sync_mode == SyncMode::Off {
2766 tracing::debug!("Skipping fsync of logical log file (synchronous=off)");
2767 self.state = CheckpointState::TruncateWal;
2768 return Ok(TransitionResult::Continue);
2769 }
2770 tracing::debug!("Fsyncing logical log file");
2771 let c = self.fsync_logical_log()?;
2772 self.state = CheckpointState::TruncateWal;
2773 if c.succeeded() {
2775 Ok(TransitionResult::Continue)
2776 } else {
2777 Ok(TransitionResult::Io(IOCompletions::Single(c)))
2778 }
2779 }
2780
2781 CheckpointState::CheckpointWal => {
2782 tracing::debug!("Performing TRUNCATE checkpoint on WAL");
2783 match self.checkpoint_wal()? {
2784 IOResult::Done(result) => {
2785 self.checkpoint_result = Some(result);
2786 self.state = CheckpointState::SyncDbFile;
2787 Ok(TransitionResult::Continue)
2788 }
2789 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
2790 }
2791 }
2792
2793 CheckpointState::SyncDbFile => {
2794 if self.sync_mode == SyncMode::Off {
2798 tracing::debug!("Skipping fsync of database file (synchronous=off)");
2799 self.state = CheckpointState::TruncateLogicalLog;
2800 return Ok(TransitionResult::Continue);
2801 }
2802
2803 let checkpoint_result = self
2804 .checkpoint_result
2805 .as_mut()
2806 .expect("checkpoint_result should be set");
2807
2808 if checkpoint_result.wal_checkpoint_backfilled == 0 {
2810 self.state = CheckpointState::TruncateLogicalLog;
2811 return Ok(TransitionResult::Continue);
2812 }
2813
2814 if checkpoint_result.db_sync_sent {
2816 self.state = CheckpointState::TruncateLogicalLog;
2817 return Ok(TransitionResult::Continue);
2818 }
2819
2820 tracing::debug!("Fsyncing database file before WAL truncation");
2821 let c = self
2822 .pager
2823 .db_file
2824 .sync(Completion::new_sync(|_| {}), self.pager.get_sync_type())?;
2825 checkpoint_result.db_sync_sent = true;
2826 Ok(TransitionResult::Io(IOCompletions::Single(c)))
2827 }
2828
2829 CheckpointState::TruncateWal => {
2830 if self.mode.should_restart_log() {
2831 if !self.lock_states.blocking_checkpoint_lock_held {
2835 if !self.checkpoint_lock.write() {
2836 return Err(crate::LimboError::Busy);
2837 }
2838 self.lock_states.blocking_checkpoint_lock_held = true;
2839 }
2840 let Some(wal) = &self.pager.wal else {
2843 panic!("No WAL to truncate");
2844 };
2845 let checkpoint_result = self
2846 .checkpoint_result
2847 .as_mut()
2848 .expect("checkpoint_result should be set");
2849 if let IOResult::IO(io) =
2850 wal.truncate_wal(checkpoint_result, self.pager.get_sync_type())?
2851 {
2852 return Ok(TransitionResult::Io(io));
2853 }
2854 }
2855 turso_assert!(
2863 self.created_btrees.is_empty() && self.staged_roots.is_empty(),
2864 "checkpoint finalized with un-published staged schema roots"
2865 );
2866 self.mvstore
2867 .durable_txid_max
2868 .store(self.durable_txid_max_new, Ordering::SeqCst);
2869 let (seq, _) = self.pager.wal_pos();
2873 let backfill =
2874 WalPos::from_pair((seq, self.pager.wal_backfill_frame().unwrap_or(0)));
2875 *self.mvstore.backfill_floor.write() = backfill;
2876 let lwm = self.mvstore.compute_lwm();
2877 self.mvstore.gc_rootpage_entries(lwm);
2879 self.state = CheckpointState::GcTableRows { next_index: 0, lwm };
2880 Ok(TransitionResult::Continue)
2881 }
2882
2883 CheckpointState::GcTableRows { .. } => {
2884 if let Some(io) = self.gc_checkpointed_table_versions() {
2885 return Ok(TransitionResult::Io(io));
2886 }
2887 let CheckpointState::GcTableRows { lwm, .. } = self.state else {
2888 unreachable!("state is GcTableRows here");
2889 };
2890 self.state = CheckpointState::GcIndexRows { next_index: 0, lwm };
2891 Ok(TransitionResult::Continue)
2892 }
2893
2894 CheckpointState::GcIndexRows { .. } => {
2895 if let Some(io) = self.gc_checkpointed_index_versions() {
2896 return Ok(TransitionResult::Io(io));
2897 }
2898 self.state = CheckpointState::Finalize;
2899 Ok(TransitionResult::Continue)
2900 }
2901
2902 CheckpointState::Finalize => {
2903 if self.lock_states.blocking_checkpoint_lock_held {
2904 tracing::debug!("Releasing blocking checkpoint lock");
2907 self.mvstore.drop_unused_row_versions_and_slots();
2908 self.checkpoint_lock.unlock();
2909 self.lock_states.blocking_checkpoint_lock_held = false;
2910 } else {
2911 self.mvstore.drop_unused_row_versions();
2913 }
2914 if self.owns_checkpoint_in_progress {
2916 self.mvstore
2917 .checkpoint_in_progress
2918 .store(false, Ordering::Release);
2919 self.owns_checkpoint_in_progress = false;
2920 }
2921 self.finalize(&())?;
2922 Ok(TransitionResult::Done(
2923 self.checkpoint_result.take().ok_or_else(|| {
2924 LimboError::InternalError("checkpoint_result not set".to_string())
2925 })?,
2926 ))
2927 }
2928 }
2929 }
2930}
2931
2932impl<Clock: LogicalClock, A: ConcurrentAllocator> StateTransition
2933 for CheckpointStateMachine<Clock, A>
2934{
2935 type Context = ();
2936 type SMResult = CheckpointResult;
2937
2938 fn step(&mut self, _context: &Self::Context) -> Result<TransitionResult<Self::SMResult>> {
2939 let res = self.step_inner(&());
2940 match res {
2941 Err(ref err) => {
2942 tracing::debug!("Error in checkpoint state machine: {err}");
2943 self.cleanup_after_external_io_error(err.clone())?;
2947 res
2948 }
2949 Ok(TransitionResult::Done(ref result)) => {
2950 self.mvstore.storage.on_checkpoint_end(Ok(result))?;
2951 res
2952 }
2953 Ok(result) => Ok(result),
2954 }
2955 }
2956
2957 fn finalize(&mut self, _context: &Self::Context) -> Result<()> {
2958 Ok(())
2959 }
2960
2961 fn is_finalized(&self) -> bool {
2962 matches!(self.state, CheckpointState::Finalize)
2963 }
2964}
2965
2966enum BuildLocalSchemaViewState {
2971 Rewind,
2972 ReadRowid,
2973 ReadRecord { rowid: i64 },
2974 Advance,
2975 MergeMvccDelta,
2976 Done,
2977}
2978
2979pub struct BuildLocalSchemaViewStateMachine<
2980 Clock: LogicalClock,
2981 A: ConcurrentAllocator = TursoAllocator,
2982> {
2983 cursor: BTreeCursor,
2984 mvstore: Arc<MvStore<Clock, A>>,
2985 connection: Arc<Connection>,
2986 snapshot_ts: u64,
2987 state: BuildLocalSchemaViewState,
2988 rows: HashMap<i64, ImmutableRecord>,
2989 finalized: bool,
2990}
2991
2992impl<Clock: LogicalClock, A: ConcurrentAllocator> BuildLocalSchemaViewStateMachine<Clock, A> {
2993 fn new(
2994 cursor: BTreeCursor,
2995 mvstore: Arc<MvStore<Clock, A>>,
2996 connection: Arc<Connection>,
2997 snapshot_ts: u64,
2998 ) -> Self {
2999 Self {
3000 cursor,
3001 mvstore,
3002 connection,
3003 snapshot_ts,
3004 state: BuildLocalSchemaViewState::Rewind,
3005 rows: HashMap::default(),
3006 finalized: false,
3007 }
3008 }
3009
3010 fn merge_mvcc_delta(&mut self) {
3014 let snapshot_ts = self.snapshot_ts;
3015 for entry in self.mvstore.rows.iter() {
3016 let key = entry.key();
3017 if key.table_id != SQLITE_SCHEMA_MVCC_TABLE_ID {
3018 continue;
3019 }
3020 let rowid = key.row_id.to_int_or_panic();
3021 let versions = entry.value().read();
3022 let present = versions.iter().find(|version| {
3023 let begin_committed = matches!(
3024 version.begin(),
3025 Some(TxTimestampOrID::Timestamp(b)) if b <= snapshot_ts
3026 );
3027 if !begin_committed {
3028 return false;
3029 }
3030 match version.end() {
3031 None => true,
3032 Some(TxTimestampOrID::Timestamp(e)) => e > snapshot_ts,
3033 Some(TxTimestampOrID::TxID(_)) => true,
3034 }
3035 });
3036 match present {
3037 Some(version) => {
3038 let data = version
3039 .row
3040 .data
3041 .as_ref()
3042 .expect("present schema version must carry row data at snapshot_ts");
3043 self.rows
3044 .insert(rowid, ImmutableRecord::from_bin_record(data.to_vec()));
3045 }
3046 None => {
3047 let existed_and_gone = versions.iter().any(|version| {
3048 matches!(
3049 version.begin(),
3050 Some(TxTimestampOrID::Timestamp(b)) if b <= snapshot_ts
3051 ) || matches!(
3052 version.end(),
3053 Some(TxTimestampOrID::Timestamp(e)) if e <= snapshot_ts
3054 )
3055 });
3056 if existed_and_gone {
3057 self.rows.remove(&rowid);
3058 }
3059 }
3060 }
3061 }
3062 }
3063}
3064
3065impl<Clock: LogicalClock, A: ConcurrentAllocator> StateTransition
3066 for BuildLocalSchemaViewStateMachine<Clock, A>
3067{
3068 type Context = ();
3069 type SMResult = Arc<Schema>;
3070
3071 fn step(&mut self, _context: &()) -> Result<TransitionResult<Self::SMResult>> {
3072 match self.state {
3073 BuildLocalSchemaViewState::Rewind => match self.cursor.rewind()? {
3074 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
3075 IOResult::Done(()) => {
3076 self.state = BuildLocalSchemaViewState::ReadRowid;
3077 Ok(TransitionResult::Continue)
3078 }
3079 },
3080 BuildLocalSchemaViewState::ReadRowid => {
3081 if !self.cursor.has_record() {
3082 self.state = BuildLocalSchemaViewState::MergeMvccDelta;
3083 return Ok(TransitionResult::Continue);
3084 }
3085 match self.cursor.rowid()? {
3086 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
3087 IOResult::Done(Some(rowid)) => {
3088 self.state = BuildLocalSchemaViewState::ReadRecord { rowid };
3089 Ok(TransitionResult::Continue)
3090 }
3091 IOResult::Done(None) => {
3092 self.state = BuildLocalSchemaViewState::Advance;
3093 Ok(TransitionResult::Continue)
3094 }
3095 }
3096 }
3097 BuildLocalSchemaViewState::ReadRecord { rowid } => {
3098 let record = match self.cursor.record()? {
3099 IOResult::IO(io) => return Ok(TransitionResult::Io(io)),
3100 IOResult::Done(Some(record)) => Some(record.clone()),
3101 IOResult::Done(None) => None,
3102 };
3103 if let Some(record) = record {
3104 self.rows.insert(rowid, record);
3105 }
3106 self.state = BuildLocalSchemaViewState::Advance;
3107 Ok(TransitionResult::Continue)
3108 }
3109 BuildLocalSchemaViewState::Advance => match self.cursor.next()? {
3110 IOResult::IO(io) => Ok(TransitionResult::Io(io)),
3111 IOResult::Done(()) => {
3112 self.state = BuildLocalSchemaViewState::ReadRowid;
3113 Ok(TransitionResult::Continue)
3114 }
3115 },
3116 BuildLocalSchemaViewState::MergeMvccDelta => {
3117 self.merge_mvcc_delta();
3118 self.state = BuildLocalSchemaViewState::Done;
3119 Ok(TransitionResult::Continue)
3120 }
3121 BuildLocalSchemaViewState::Done => {
3122 self.finalized = true;
3123 let schema =
3124 self.mvstore
3125 .build_schema_from_rows(&self.connection, &self.rows, &[])?;
3126 Ok(TransitionResult::Done(schema))
3127 }
3128 }
3129 }
3130
3131 fn finalize(&mut self, _context: &()) -> Result<()> {
3132 self.finalized = true;
3133 Ok(())
3134 }
3135
3136 fn is_finalized(&self) -> bool {
3137 self.finalized
3138 }
3139}
3140
3141#[cfg(clt_turso_tests)]
3142mod tests {
3143 use super::*;
3144 use crate::alloc::vec;
3145 use crate::mvcc::database::tests::MvccTestDbNoConn;
3146 use crate::mvcc::database::SortableIndexKey;
3147 use crate::translate::collate::CollationSeq;
3148 use crate::types::{IndexInfo, KeyInfo};
3149 use turso_parser::ast::SortOrder;
3150
3151 fn sqlite_schema_row_version(
3152 rowid: i64,
3153 entry_type: &'static str,
3154 name: &'static str,
3155 table_name: &'static str,
3156 root_page: i64,
3157 begin: Option<u64>,
3158 end: Option<u64>,
3159 ) -> RowVersion {
3160 let record = ImmutableRecord::from_values(
3161 &[
3162 Value::build_text(entry_type),
3163 Value::build_text(name),
3164 Value::build_text(table_name),
3165 Value::from_i64(root_page),
3166 Value::build_text(format!("sql:{entry_type}:{name}:{root_page}")),
3167 ],
3168 5,
3169 )
3170 .unwrap();
3171 RowVersion {
3172 id: 1,
3173 begin: crate::mvcc::database::PackedTs::pack(begin.map(TxTimestampOrID::Timestamp)),
3174 end: crate::mvcc::database::PackedTs::pack(end.map(TxTimestampOrID::Timestamp)),
3175 row: Row::new_table_row(
3176 RowID::new(SQLITE_SCHEMA_MVCC_TABLE_ID, RowKey::Int(rowid)),
3177 record.as_blob(),
3178 5,
3179 )
3180 .unwrap(),
3181 btree_resident: false,
3182 materialized_at: crate::mvcc::database::WalPos::ORIGIN,
3183 }
3184 }
3185
3186 #[test]
3187 fn sqlite_schema_identity_treats_index_sql_rewrite_as_same_object() {
3188 let old = sqlite_schema_row_version(3, "index", "idx_t_a", "t", 7, Some(1), Some(2));
3189 let new = sqlite_schema_row_version(3, "index", "idx_t_a", "t", 7, Some(2), None);
3190
3191 assert_eq!(
3192 sqlite_schema_btree_identity(&old),
3193 Some(SqliteSchemaBtreeIdentity {
3194 kind: SqliteSchemaBtreeKind::Index,
3195 root_page: 7,
3196 })
3197 );
3198 assert!(sqlite_schema_versions_refer_to_btree(&old, &new));
3199 assert!(!is_schema_metadata_only_rewrite(&old, Some(&new)));
3200 }
3201
3202 #[test]
3203 fn sqlite_schema_identity_treats_table_sql_rewrite_as_same_object() {
3204 let old = sqlite_schema_row_version(2, "table", "t", "t", 5, Some(1), Some(2));
3205 let new = sqlite_schema_row_version(2, "table", "t", "t", 5, Some(2), None);
3206
3207 assert!(sqlite_schema_versions_refer_to_btree(&old, &new));
3208 assert!(!is_schema_metadata_only_rewrite(&old, Some(&new)));
3209 }
3210
3211 #[test]
3212 fn sqlite_schema_identity_detects_drop_recreate_as_different_objects() {
3213 let dropped = sqlite_schema_row_version(3, "index", "idx_t_v", "t", -4, Some(1), Some(2));
3214 let recreated = sqlite_schema_row_version(3, "index", "idx_t_v", "t", -5, Some(2), None);
3215
3216 assert!(!sqlite_schema_versions_refer_to_btree(&dropped, &recreated));
3217 assert!(is_schema_metadata_only_rewrite(&dropped, Some(&recreated)));
3218 }
3219
3220 #[test]
3221 fn sqlite_schema_identity_detects_drop_without_successor() {
3222 let dropped = sqlite_schema_row_version(3, "index", "idx_t_v", "t", 11, Some(1), Some(2));
3223
3224 assert!(is_schema_metadata_only_rewrite(&dropped, None));
3225 }
3226
3227 #[test]
3228 fn sqlite_schema_identity_ignores_non_btree_schema_entries() {
3229 let trigger = sqlite_schema_row_version(9, "trigger", "trg_t", "t", 0, Some(1), Some(2));
3230 let rewritten_trigger =
3231 sqlite_schema_row_version(9, "trigger", "trg_t", "t", 0, Some(2), None);
3232
3233 assert_eq!(sqlite_schema_btree_identity(&trigger), None);
3234 assert!(!sqlite_schema_versions_refer_to_btree(
3235 &trigger,
3236 &rewritten_trigger
3237 ));
3238 assert!(!is_schema_metadata_only_rewrite(
3239 &trigger,
3240 Some(&rewritten_trigger)
3241 ));
3242 }
3243
3244 #[test]
3245 fn sqlite_schema_identity_ignores_payloadless_tombstones() {
3246 let tombstone = RowVersion {
3247 id: 1,
3248 begin: crate::mvcc::database::PackedTs::pack(None),
3249 end: crate::mvcc::database::PackedTs::pack(Some(TxTimestampOrID::Timestamp(2))),
3250 row: Row::new_table_row(
3251 RowID::new(SQLITE_SCHEMA_MVCC_TABLE_ID, RowKey::Int(9)),
3252 &[],
3253 0,
3254 )
3255 .unwrap(),
3256 btree_resident: false,
3257 materialized_at: crate::mvcc::database::WalPos::ORIGIN,
3258 };
3259
3260 assert_eq!(sqlite_schema_btree_identity(&tombstone), None);
3261 assert!(!is_schema_metadata_only_rewrite(&tombstone, None));
3262 }
3263
3264 fn index_row_version(
3265 index_id: MVTableId,
3266 key_text: &str,
3267 rowid: i64,
3268 version_id: u64,
3269 begin: Option<u64>,
3270 end: Option<u64>,
3271 btree_resident: bool,
3272 ) -> (Arc<SortableIndexKey>, RowVersion) {
3273 let index_info = Arc::new(
3274 IndexInfo::new(
3275 vec![
3276 KeyInfo {
3277 sort_order: SortOrder::Asc,
3278 collation: CollationSeq::Binary,
3279 nulls_order: None,
3280 },
3281 KeyInfo {
3282 sort_order: SortOrder::Asc,
3283 collation: CollationSeq::Binary,
3284 nulls_order: None,
3285 },
3286 ],
3287 true,
3288 2,
3289 true,
3290 )
3291 .unwrap(),
3292 );
3293 let key_record = ImmutableRecord::from_values(
3294 &[
3295 Value::Text(crate::types::Text::new(key_text.to_string())),
3296 Value::from_i64(rowid),
3297 ],
3298 2,
3299 )
3300 .unwrap();
3301 let sortable_key = SortableIndexKey::new_from_record(key_record, index_info);
3302 let key_arc = Arc::new(sortable_key.clone());
3303 let row = Row::new_index_row(
3304 RowID::new(index_id, RowKey::Record(Arc::new(sortable_key))),
3305 2,
3306 );
3307 let row_version = RowVersion {
3308 id: version_id,
3309 begin: crate::mvcc::database::PackedTs::pack(begin.map(TxTimestampOrID::Timestamp)),
3310 end: crate::mvcc::database::PackedTs::pack(end.map(TxTimestampOrID::Timestamp)),
3311 row,
3312 btree_resident,
3313 materialized_at: crate::mvcc::database::WalPos::ORIGIN,
3314 };
3315 (key_arc, row_version)
3316 }
3317
3318 #[test]
3319 fn checkpoint_retry_does_not_replay_checkpointed_btree_resident_delete() {
3320 let db = MvccTestDbNoConn::new();
3321 let conn = db.connect();
3322 let mvstore = db.get_mvcc_store();
3323 let pager = conn.pager.load().clone();
3324 let mut checkpoint = CheckpointStateMachine::new(
3325 pager,
3326 mvstore.clone(),
3327 conn.clone(),
3328 true,
3329 conn.get_sync_mode(),
3330 crate::MAIN_DB_ID,
3331 CheckpointMode::Truncate {
3332 upper_bound_inclusive: None,
3333 },
3334 );
3335 checkpoint.durable_txid_max_old = std::num::NonZeroU64::new(10);
3336 checkpoint.durable_txid_max_new = 10;
3337
3338 let index_id = MVTableId::from(-42);
3339 let (garbage_key, garbage_version) =
3340 index_row_version(index_id, "blue_river_906", 75, 1, None, None, true);
3341 let (_, tombstone_version) =
3342 index_row_version(index_id, "blue_river_906", 75, 2, None, Some(10), true);
3343
3344 mvstore
3345 .insert_index_version(index_id, garbage_key, garbage_version)
3346 .unwrap();
3347 let entry = mvstore
3348 .index_rows
3349 .get(&index_id)
3350 .expect("index entry should exist after first insert");
3351 let tombstone_key = entry
3352 .value()
3353 .front()
3354 .expect("key bucket should exist after first insert")
3355 .key()
3356 .clone();
3357 mvstore
3358 .insert_index_version(index_id, tombstone_key, tombstone_version)
3359 .unwrap();
3360
3361 while checkpoint.collect_index_rows().unwrap().is_some() {}
3362
3363 assert!(
3364 checkpoint.index_write_set.is_empty(),
3365 "a retry checkpoint must not replay a delete whose btree_resident tombstone was already made durable"
3366 );
3367 }
3368
3369 fn committed_table_row_version(table_id: MVTableId, rowid: i64) -> RowVersion {
3370 table_row_version(table_id, rowid, 1, Some(5), None, false)
3371 }
3372
3373 fn table_row_version(
3374 table_id: MVTableId,
3375 rowid: i64,
3376 version_id: u64,
3377 begin: Option<u64>,
3378 end: Option<u64>,
3379 btree_resident: bool,
3380 ) -> RowVersion {
3381 let record = ImmutableRecord::from_values(&[Value::from_i64(rowid)], 1).unwrap();
3382 RowVersion {
3383 id: version_id,
3384 begin: crate::mvcc::database::PackedTs::pack(begin.map(TxTimestampOrID::Timestamp)),
3385 end: crate::mvcc::database::PackedTs::pack(end.map(TxTimestampOrID::Timestamp)),
3386 row: Row::new_table_row(
3387 RowID::new(table_id, RowKey::Int(rowid)),
3388 record.as_blob(),
3389 1,
3390 )
3391 .unwrap(),
3392 btree_resident,
3393 materialized_at: crate::mvcc::database::WalPos::ORIGIN,
3394 }
3395 }
3396
3397 fn checkpoint_for_collect_tests(
3398 ) -> CheckpointStateMachine<crate::mvcc::clock::MvccClock, crate::alloc::DynAllocator> {
3399 let db = MvccTestDbNoConn::new();
3400 let conn = db.connect();
3401 let mvstore = db.get_mvcc_store();
3402 let pager = conn.pager.load().clone();
3403 let mut checkpoint = CheckpointStateMachine::new(
3404 pager,
3405 mvstore,
3406 conn.clone(),
3407 true,
3408 conn.get_sync_mode(),
3409 crate::MAIN_DB_ID,
3410 CheckpointMode::Truncate {
3411 upper_bound_inclusive: None,
3412 },
3413 );
3414 checkpoint.durable_txid_max_old = NonZeroU64::new(2);
3415 checkpoint
3416 }
3417
3418 #[test]
3419 fn checkpoint_collection_uses_btree_marker_for_existence_but_writes_surviving_replacement() {
3420 let checkpoint = checkpoint_for_collect_tests();
3421 let table_id = MVTableId::from(-2);
3422 let btree_tombstone = table_row_version(table_id, 1, 1, None, Some(5), true);
3423 let replacement = table_row_version(table_id, 1, 2, Some(5), None, false);
3424
3425 let checkpointable =
3426 checkpoint.maybe_get_checkpointable_versions(&[btree_tombstone, replacement], table_id);
3427
3428 assert_eq!(checkpointable.len(), 1);
3429 assert_eq!(checkpointable[0].id, 2);
3430 assert_eq!(checkpointable[0].end(), None);
3431 }
3432
3433 #[test]
3434 fn checkpoint_collection_uses_btree_marker_for_later_delete_of_replacement() {
3435 let checkpoint = checkpoint_for_collect_tests();
3436 let table_id = MVTableId::from(-2);
3437 let btree_tombstone = table_row_version(table_id, 1, 1, None, Some(5), true);
3438 let deleted_replacement = table_row_version(table_id, 1, 2, Some(5), Some(6), false);
3439
3440 let checkpointable = checkpoint
3441 .maybe_get_checkpointable_versions(&[btree_tombstone, deleted_replacement], table_id);
3442
3443 assert_eq!(checkpointable.len(), 1);
3444 assert_eq!(checkpointable[0].id, 2);
3445 assert_eq!(checkpointable[0].end(), Some(TxTimestampOrID::Timestamp(6)));
3446 }
3447
3448 #[test]
3449 fn checkpoint_collection_skips_delete_of_never_checkpointed_replacement_without_btree_marker() {
3450 let checkpoint = checkpoint_for_collect_tests();
3451 let table_id = MVTableId::from(-2);
3452 let deleted_replacement = table_row_version(table_id, 1, 2, Some(5), Some(6), false);
3453
3454 let checkpointable =
3455 checkpoint.maybe_get_checkpointable_versions(&[deleted_replacement], table_id);
3456
3457 assert!(checkpointable.is_empty());
3458 }
3459
3460 #[test]
3461 fn collect_table_rows_preempts_on_large_scan() {
3462 let db = MvccTestDbNoConn::new();
3463 let conn = db.connect();
3464 let mvstore = db.get_mvcc_store();
3465 let pager = conn.pager.load().clone();
3466 let mut checkpoint = CheckpointStateMachine::new(
3467 pager,
3468 mvstore.clone(),
3469 conn.clone(),
3470 true,
3471 conn.get_sync_mode(),
3472 crate::MAIN_DB_ID,
3473 CheckpointMode::Truncate {
3474 upper_bound_inclusive: None,
3475 },
3476 );
3477
3478 let table_id = MVTableId::from(-2);
3480 let row_count = COLLECT_PREEMPTION_THRESHOLD + 10;
3481 for i in 0..row_count as i64 {
3482 let version = committed_table_row_version(table_id, i);
3483 let mut versions =
3484 <crate::mvcc::database::RowVersionChain<crate::alloc::DynAllocator> as crate::alloc::TursoVecInExt<
3485 RowVersion,
3486 crate::alloc::DynAllocator,
3487 >>::new_in(crate::alloc::DynAllocator::default());
3488 versions.push(version);
3489 mvstore.rows.insert(
3490 RowID::new(table_id, RowKey::Int(i)),
3491 Arc::new(RwLock::new(versions)),
3492 );
3493 }
3494
3495 let first = checkpoint.collect_table_rows().unwrap();
3497 assert!(
3498 first.is_some_and(|io| io.is_explicit_yield()),
3499 "scanning more than COLLECT_PREEMPTION_THRESHOLD rows must preempt with an explicit yield"
3500 );
3501
3502 while checkpoint.collect_table_rows().unwrap().is_some() {}
3505 assert_eq!(checkpoint.write_set.len(), row_count);
3506 }
3507
3508 #[test]
3509 fn collect_index_rows_preempts_on_large_scan() {
3510 let db = MvccTestDbNoConn::new();
3511 let conn = db.connect();
3512 let mvstore = db.get_mvcc_store();
3513 let pager = conn.pager.load().clone();
3514 let mut checkpoint = CheckpointStateMachine::new(
3515 pager,
3516 mvstore.clone(),
3517 conn.clone(),
3518 true,
3519 conn.get_sync_mode(),
3520 crate::MAIN_DB_ID,
3521 CheckpointMode::Truncate {
3522 upper_bound_inclusive: None,
3523 },
3524 );
3525
3526 let index_id = MVTableId::from(-7);
3527 let row_count = COLLECT_PREEMPTION_THRESHOLD + 10;
3528 for i in 0..row_count as i64 {
3529 let (key, version) = index_row_version(index_id, "k", i, 1, Some(5), None, false);
3530 mvstore
3531 .insert_index_version(index_id, key, version)
3532 .unwrap();
3533 }
3534
3535 let first = checkpoint.collect_index_rows().unwrap();
3536 assert!(
3537 first.is_some_and(|io| io.is_explicit_yield()),
3538 "scanning more than COLLECT_PREEMPTION_THRESHOLD index rows must preempt with an explicit yield"
3539 );
3540
3541 while checkpoint.collect_index_rows().unwrap().is_some() {}
3542 assert_eq!(checkpoint.index_write_set.len(), row_count);
3543 }
3544
3545 #[test]
3546 fn gc_checkpointed_table_versions_preempts_on_large_scan() {
3547 let db = MvccTestDbNoConn::new();
3548 let conn = db.connect();
3549 let mvstore = db.get_mvcc_store();
3550 let pager = conn.pager.load().clone();
3551 let mut checkpoint = CheckpointStateMachine::new(
3552 pager,
3553 mvstore.clone(),
3554 conn.clone(),
3555 true,
3556 conn.get_sync_mode(),
3557 crate::MAIN_DB_ID,
3558 CheckpointMode::Truncate {
3559 upper_bound_inclusive: None,
3560 },
3561 );
3562 checkpoint.lock_states.blocking_checkpoint_lock_held = true;
3563 checkpoint.durable_txid_max_new = 5;
3564
3565 let table_id = MVTableId::from(-2);
3566 let row_count = COLLECT_PREEMPTION_THRESHOLD + 10;
3567 for i in 0..row_count as i64 {
3568 let version = committed_table_row_version(table_id, i);
3569 let row_id = RowID::new(table_id, RowKey::Int(i));
3570 let mut versions =
3571 <crate::mvcc::database::RowVersionChain<crate::alloc::DynAllocator> as crate::alloc::TursoVecInExt<
3572 RowVersion,
3573 crate::alloc::DynAllocator,
3574 >>::new_in(crate::alloc::DynAllocator::default());
3575 versions.push(version.clone());
3576 mvstore.rows.insert(row_id, Arc::new(RwLock::new(versions)));
3577 checkpoint.write_set.push((version, None));
3578 }
3579 checkpoint.state = CheckpointState::GcTableRows {
3580 next_index: 0,
3581 lwm: u64::MAX,
3582 };
3583
3584 let first = checkpoint.gc_checkpointed_table_versions();
3585 assert!(
3586 first.is_some_and(|io| io.is_explicit_yield()),
3587 "GCing more than COLLECT_PREEMPTION_THRESHOLD rows must preempt with an explicit yield"
3588 );
3589
3590 while checkpoint.gc_checkpointed_table_versions().is_some() {}
3591 let remaining = mvstore
3592 .rows
3593 .iter()
3594 .filter(|entry| entry.key().table_id == table_id)
3595 .count();
3596 assert_eq!(remaining, 0);
3597 }
3598
3599 #[test]
3600 fn gc_checkpointed_index_versions_preempts_on_large_scan() {
3601 let db = MvccTestDbNoConn::new();
3602 let conn = db.connect();
3603 let mvstore = db.get_mvcc_store();
3604 let pager = conn.pager.load().clone();
3605 let mut checkpoint = CheckpointStateMachine::new(
3606 pager,
3607 mvstore.clone(),
3608 conn.clone(),
3609 true,
3610 conn.get_sync_mode(),
3611 crate::MAIN_DB_ID,
3612 CheckpointMode::Truncate {
3613 upper_bound_inclusive: None,
3614 },
3615 );
3616 checkpoint.lock_states.blocking_checkpoint_lock_held = true;
3617 checkpoint.durable_txid_max_new = 5;
3618
3619 let index_id = MVTableId::from(-7);
3620 let row_count = COLLECT_PREEMPTION_THRESHOLD + 10;
3621 for i in 0..row_count as i64 {
3622 let (key, version) = index_row_version(index_id, "k", i, 1, Some(5), None, false);
3623 mvstore
3624 .insert_index_version(index_id, key, version.clone())
3625 .unwrap();
3626 checkpoint.index_write_set.push((index_id, version, false));
3627 }
3628 checkpoint.state = CheckpointState::GcIndexRows {
3629 next_index: 0,
3630 lwm: u64::MAX,
3631 };
3632
3633 let first = checkpoint.gc_checkpointed_index_versions();
3634 assert!(
3635 first.is_some_and(|io| io.is_explicit_yield()),
3636 "GCing more than COLLECT_PREEMPTION_THRESHOLD index rows must preempt with an explicit yield"
3637 );
3638
3639 while checkpoint.gc_checkpointed_index_versions().is_some() {}
3640 let remaining = mvstore
3641 .index_rows
3642 .get(&index_id)
3643 .map_or(0, |entry| entry.value().len());
3644 assert_eq!(remaining, 0);
3645 }
3646}