Skip to main content

clt_database/mvcc/
cursor.rs

1use crate::alloc::{ConcurrentAllocator, TryReserveError, TursoAllocator};
2use crate::skiplist::{comparator::BasicComparator, map::Entry};
3use crate::turso_assert;
4
5use crate::mvcc::clock::LogicalClock;
6use crate::mvcc::database::{
7    create_seek_range, MVTableId, MvStore, Row, RowID, RowKey, RowVersions, SortableIndexKey,
8};
9#[cfg(any(clt_turso_tests, injected_yields))]
10use crate::mvcc::yield_hooks::{ProvidesYieldContext, YieldContext, YieldPointMarker};
11use crate::mvcc::yield_points::inject_io_yield;
12use crate::storage::btree::{BTreeCursor, BTreeKey, CursorTrait};
13use crate::sync::Arc;
14use crate::translate::plan::IterationDirection;
15use crate::types::{
16    compare_immutable, IOCompletions, IOResult, ImmutableRecord, IndexInfo, SeekKey, SeekOp,
17    SeekResult, Value,
18};
19use crate::vdbe::make_record;
20use crate::vdbe::Register;
21use crate::{return_if_io, Completion, Connection, LimboError, Pager, Result};
22use std::any::Any;
23use std::fmt::Debug;
24use std::ops::Bound;
25#[cfg(any(clt_turso_tests, injected_yields))]
26use strum::EnumCount;
27
28#[derive(Clone)]
29enum CursorPosition<A: ConcurrentAllocator = TursoAllocator> {
30    /// We haven't loaded any row yet.
31    BeforeFirst,
32    /// We have loaded a row. This position points to a rowid in either MVCC index or in BTree.
33    Loaded {
34        row_id: RowID,
35        /// Indicates whether the rowid is pointing BTreeCursor or MVCC index.
36        in_btree: bool,
37        /// Resolved MVCC version chain for this row, captured from the range
38        /// iterator so `read_mvcc_current_row` can skip a second `self.rows.get`.
39        /// `Some` only for MVCC table rows reached via the scan path; `None`
40        /// (btree rows, index rows, seek/insert positions) falls back to a lookup.
41        versions: Option<RowVersions<A>>,
42    },
43    /// We have reached the end of the table.
44    End,
45}
46
47impl<A: ConcurrentAllocator> Debug for CursorPosition<A> {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Self::BeforeFirst => f.write_str("BeforeFirst"),
51            Self::Loaded {
52                row_id, in_btree, ..
53            } => f
54                .debug_struct("Loaded")
55                .field("row_id", row_id)
56                .field("in_btree", in_btree)
57                .finish_non_exhaustive(),
58            Self::End => f.write_str("End"),
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy)]
64enum ExistsState {
65    ExistsBtree,
66}
67
68#[derive(Debug, Clone, Copy)]
69/// State machine for advancing the btree cursor.
70/// Advancing means advancing the btree iterator that could be going either forwards or backwards.
71enum AdvanceBtreeState {
72    RewindCheckBtreeKey, // Check if first key found is valid
73    NextBtree,           // Advance to next key
74    NextCheckBtreeKey,   // Check if next key found is valid, if it isn't go back to NextBtree
75}
76
77#[derive(Debug, Clone, Copy)]
78/// Rewind state is used to track the state of the rewind **AND** last operation. Since both seem to do similiar
79/// operations we can use the same enum for both.
80enum RewindState {
81    Advance,
82}
83
84#[derive(Debug, Clone, Copy)]
85enum NextState {
86    AdvanceUnitialized,
87    CheckNeedsAdvance,
88    Advance,
89}
90#[derive(Debug, Clone, Copy)]
91enum PrevState {
92    AdvanceUnitialized,
93    CheckNeedsAdvance,
94    Advance,
95}
96
97#[derive(Debug, Clone, Copy)]
98enum SeekBtreeState {
99    /// Seeking in btree (MVCC seek already done)
100    SeekBtree,
101    /// Advance to next key in btree (if we got [SeekResult::TryAdvance], or the current row is shadowed by MVCC)
102    AdvanceBTree,
103    /// Check if current row is visible (not shadowed by MVCC)
104    CheckRow,
105}
106
107#[derive(Debug, Clone, Copy)]
108enum SeekState {
109    /// Seeking in btree (MVCC seek already done)
110    SeekBtree(SeekBtreeState),
111    /// Pick winner and finalize
112    PickWinner,
113}
114
115#[derive(Debug, Clone, Copy)]
116enum CountState {
117    Rewind,
118    NextBtree { count: usize },
119    CheckBtreeKey { count: usize },
120}
121#[derive(Debug, Clone)]
122enum MvccLazyCursorState {
123    Next(NextState),
124    Prev(PrevState),
125    Rewind(RewindState),
126    Exists(ExistsState),
127    Seek(SeekState, IterationDirection),
128}
129
130#[cfg(any(clt_turso_tests, injected_yields))]
131#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumCount)]
132#[repr(u8)]
133pub(crate) enum CursorYieldPoint {
134    NextStart,
135    NextBtreeAdvance,
136    PrevBtreeAdvance,
137    SeekStart,
138    SeekBtreeProgress,
139    ExistsBtreeFallback,
140    CountProgress,
141    AdvanceBtreeForwardProgress,
142    AdvanceBtreeBackwardProgress,
143}
144
145#[cfg(any(clt_turso_tests, injected_yields))]
146impl YieldPointMarker for CursorYieldPoint {
147    const POINT_COUNT: u8 = Self::COUNT as u8;
148
149    fn ordinal(self) -> u8 {
150        self as u8
151    }
152}
153
154#[cfg(any(clt_turso_tests, injected_yields))]
155impl<Clock: LogicalClock + 'static, A: ConcurrentAllocator> ProvidesYieldContext
156    for MvccLazyCursor<Clock, A>
157{
158    fn yield_context(&self) -> YieldContext {
159        YieldContext::new(
160            self.connection.yield_injector(),
161            self.connection.failure_injector(),
162            self.yield_instance_id,
163            cursor_yield_key(self.tx_id, self.table_id),
164        )
165    }
166}
167
168fn current_pos_matches_seek_key(
169    current_row_id: &RowKey,
170    seek_key: &SeekKey<'_>,
171    mv_cursor_type: &MvccCursorType,
172) -> Result<bool> {
173    Ok(match (current_row_id, seek_key) {
174        (RowKey::Int(current), SeekKey::TableRowId(target)) => *current == *target,
175        (RowKey::Record(current), SeekKey::IndexKey(target)) => {
176            let MvccCursorType::Index(index_info) = mv_cursor_type else {
177                return Ok(false);
178            };
179            let key_info: Vec<_> = index_info
180                .key_info
181                .iter()
182                .take(target.column_count())
183                .cloned()
184                .collect();
185            compare_immutable(target.get_values()?, current.key.get_values()?, &key_info).is_eq()
186        }
187        _ => false,
188    })
189}
190
191#[cfg(any(clt_turso_tests, injected_yields))]
192fn cursor_yield_key(tx_id: u64, table_id: MVTableId) -> u64 {
193    // ASCII-ish "CURSORCR"
194    // any large number will do
195    const CURSOR_SELECTION_TAG: u64 = 0x4355_5253_4F52_4352;
196    // Mix tx/table identity and add a per-family tag (here Cursor tag), so that we get a nice
197    // yield plans
198    // 17 here is arbitrary, any number would do.
199    tx_id ^ (i64::from(table_id) as u64).rotate_left(17) ^ CURSOR_SELECTION_TAG
200}
201
202/// We read rows from MVCC index or BTree in a dual-cursor approach.
203/// This means we read rows from both cursors and then advance the cursor that was just consumed.
204/// With DualCursorPeek we track the "peeked" next value for each cursor in the dual-cursor iteration,
205/// so that we always return the correct 'next' value (e.g. if mvcc has 1 and 3 and btree has 2 and 4,
206/// we should return 1, 2, 3, 4 in order).
207#[derive(Debug, Clone)]
208struct DualCursorPeek<A: ConcurrentAllocator = TursoAllocator> {
209    /// Next row available from MVCC
210    mvcc_peek: CursorPeek<A>,
211    /// Next row available from btree
212    btree_peek: CursorPeek<A>,
213}
214
215impl<A: ConcurrentAllocator> Default for DualCursorPeek<A> {
216    fn default() -> Self {
217        Self {
218            mvcc_peek: CursorPeek::default(),
219            btree_peek: CursorPeek::default(),
220        }
221    }
222}
223
224impl<A: ConcurrentAllocator> DualCursorPeek<A> {
225    /// Returns the next row key, whether the row is from the BTree, and (for
226    /// MVCC winners) the resolved version chain captured during iteration.
227    fn get_next(&self, dir: IterationDirection) -> Option<(RowKey, bool, Option<RowVersions<A>>)> {
228        tracing::trace!(
229            "get_next: mvcc_key: {:?}, btree_key: {:?}",
230            self.mvcc_peek.get_row_key(),
231            self.btree_peek.get_row_key()
232        );
233        match (self.mvcc_peek.get_row_key(), self.btree_peek.get_row_key()) {
234            (Some(mvcc_key), Some(btree_key)) => {
235                if dir == IterationDirection::Forwards {
236                    // In forwards iteration we want the smaller of the two keys
237                    if mvcc_key <= btree_key {
238                        Some((mvcc_key.clone(), false, self.mvcc_peek.get_versions()))
239                    } else {
240                        Some((btree_key.clone(), true, None))
241                    }
242                // In backwards iteration we want the larger of the two keys
243                } else if mvcc_key >= btree_key {
244                    Some((mvcc_key.clone(), false, self.mvcc_peek.get_versions()))
245                } else {
246                    Some((btree_key.clone(), true, None))
247                }
248            }
249            (Some(mvcc_key), None) => {
250                Some((mvcc_key.clone(), false, self.mvcc_peek.get_versions()))
251            }
252            (None, Some(btree_key)) => Some((btree_key.clone(), true, None)),
253            (None, None) => None,
254        }
255    }
256
257    /// Returns a new [CursorPosition] based on the next row key
258    pub fn cursor_position_from_next(
259        &self,
260        table_id: MVTableId,
261        dir: IterationDirection,
262    ) -> CursorPosition<A> {
263        match self.get_next(dir) {
264            Some((row_key, in_btree, versions)) => CursorPosition::Loaded {
265                row_id: RowID {
266                    table_id,
267                    row_id: row_key,
268                },
269                in_btree,
270                versions,
271            },
272            None => match dir {
273                IterationDirection::Forwards => CursorPosition::End,
274                IterationDirection::Backwards => CursorPosition::BeforeFirst,
275            },
276        }
277    }
278
279    pub fn both_uninitialized(&self) -> bool {
280        matches!(self.mvcc_peek, CursorPeek::Uninitialized)
281            && matches!(self.btree_peek, CursorPeek::Uninitialized)
282    }
283
284    pub fn btree_uninitialized(&self) -> bool {
285        matches!(self.btree_peek, CursorPeek::Uninitialized)
286    }
287
288    pub fn mvcc_exhausted(&self) -> bool {
289        matches!(self.mvcc_peek, CursorPeek::Exhausted)
290    }
291    pub fn btree_exhausted(&self) -> bool {
292        matches!(self.btree_peek, CursorPeek::Exhausted)
293    }
294}
295
296#[derive(Debug, Clone)]
297enum CursorPeek<A: ConcurrentAllocator = TursoAllocator> {
298    Uninitialized,
299    Row {
300        key: RowKey,
301        /// Resolved MVCC version chain, set when this peek came from the MVCC
302        /// table iterator. `None` for btree peeks and index peeks.
303        versions: Option<RowVersions<A>>,
304    },
305    Exhausted,
306}
307
308impl<A: ConcurrentAllocator> Default for CursorPeek<A> {
309    fn default() -> Self {
310        Self::Uninitialized
311    }
312}
313
314impl<A: ConcurrentAllocator> CursorPeek<A> {
315    pub fn get_row_key(&self) -> Option<&RowKey> {
316        match self {
317            CursorPeek::Row { key, .. } => Some(key),
318            _ => None,
319        }
320    }
321
322    pub fn get_versions(&self) -> Option<RowVersions<A>> {
323        match self {
324            CursorPeek::Row { versions, .. } => versions.clone(),
325            _ => None,
326        }
327    }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub enum MvccCursorType {
332    Table,
333    Index(Arc<IndexInfo>),
334}
335
336pub(crate) type MvccEntry<'l, T, A = TursoAllocator> =
337    Entry<'l, T, RowVersions<A>, BasicComparator, A>;
338
339pub(crate) type MvccIterator<'l, T, A = TursoAllocator> =
340    Box<dyn Iterator<Item = MvccEntry<'l, T, A>> + Send + Sync>;
341
342/// Extends the lifetime of a SkipMap iterator to `'static`.
343///
344/// # Why a macro instead of a function?
345///
346/// Rust's `crate::skiplist::map::Entry<'a, K, V>` is *invariant* over `K`, meaning
347/// the lifetime `'a` cannot be coerced through a function boundary. When we try to pass
348/// `Box<dyn Iterator<Item = Entry<'_, K, V>>>` to a function expecting a generic lifetime,
349/// the compiler cannot unify the lifetimes across the function call.
350///
351/// A macro expands inline at the call site, avoiding the function boundary entirely and
352/// allowing the explicit transmute with both source and destination types specified.
353///
354/// # Safety
355///
356/// The caller must ensure that the underlying `SkipMap` from which the iterator was created
357/// outlives the returned iterator. This is guaranteed when:
358/// - For table iterators: The `MvStore.rows` SkipMap is held in an `Arc<MvStore>` that
359///   outlives the cursor.
360/// - For index iterators: The `MvStore.index_rows` SkipMap is held in an `Arc<MvStore>`
361///   that outlives the cursor.
362macro_rules! static_iterator_hack {
363    ($iter:expr, $key_type:ty) => {
364        static_iterator_hack!($iter, $key_type, crate::alloc::TursoAllocator)
365    };
366    ($iter:expr, $key_type:ty, $alloc:ty) => {
367        // SAFETY: See macro documentation above.
368        unsafe {
369            std::mem::transmute::<
370                Box<
371                    dyn Iterator<Item = crate::mvcc::cursor::MvccEntry<'_, $key_type, $alloc>>
372                        + Send
373                        + Sync,
374                >,
375                Box<
376                    dyn Iterator<Item = crate::mvcc::cursor::MvccEntry<'static, $key_type, $alloc>>
377                        + Send
378                        + Sync,
379                >,
380            >($iter)
381        }
382    };
383}
384
385pub(crate) use static_iterator_hack;
386
387/// Forward-scan finger over `index_rows`, co-advanced with the B-tree cursor so
388/// the per-row "is this B-tree row shadowed by MVCC?" check is an amortized-O(1)
389/// merge step instead of an `index_rows.get()` (O(log N)) per scanned row.
390/// Forward index cursors only; [`reset`](Self::reset) on any reposition, since
391/// the finger is monotonic.
392#[derive(Default)]
393pub(crate) enum IndexShadowFinger<A: ConcurrentAllocator = TursoAllocator> {
394    /// Not yet created; built lazily on the next shadow check.
395    #[default]
396    Uninitialized,
397    /// Positioned at `key`, holding its version chain. The shadow bit is resolved
398    /// lazily (only when a B-tree row matches this key exactly)
399    Peeked {
400        iter: MvccIterator<'static, Arc<SortableIndexKey>, A>,
401        key: Arc<SortableIndexKey>,
402        versions: RowVersions<A>,
403    },
404    /// Ran past the last version; every remaining B-tree row is visible.
405    Exhausted,
406}
407
408impl<A: ConcurrentAllocator> IndexShadowFinger<A> {
409    /// Reset so the next shadow check rebuilds the finger. Required on any B-tree
410    /// reposition (seek/rewind): a finger left ahead of the new position would
411    /// report a shadowed row as valid.
412    fn reset(&mut self) {
413        *self = Self::Uninitialized;
414    }
415
416    /// Advance `iter` to its next entry, cloning the key and version-chain `Arc`
417    /// (both cheap) so no borrowed skiplist `Entry` is held afterward. The shadow
418    /// bit is deliberately not resolved here — see [`Self::Peeked`].
419    fn advance(mut iter: MvccIterator<'static, Arc<SortableIndexKey>, A>) -> Self {
420        match iter.next() {
421            Some(entry) => Self::Peeked {
422                key: entry.key().clone(),
423                versions: entry.value().clone(),
424                iter,
425            },
426            None => Self::Exhausted,
427        }
428    }
429
430    /// Whether the B-tree row `key` is visible (not shadowed by an MVCC version),
431    /// served from the co-positioned finger. Forward equivalent of
432    /// [`MvStore::query_btree_version_is_valid`] for index keys.
433    pub(crate) fn btree_row_is_valid<Clock: LogicalClock>(
434        &mut self,
435        db: &MvStore<Clock, A>,
436        table_id: MVTableId,
437        tx_id: u64,
438        key: &Arc<SortableIndexKey>,
439    ) -> bool {
440        if matches!(self, Self::Uninitialized) {
441            // Scoped so the skiplist guard drops before `step` re-borrows `db`.
442            let iter = {
443                // Avoid allocating skiplist here with `try_get_or_insert_with`
444                let index_rows = db.index_rows.get(&table_id);
445                // Seed the finger at the first index key >= the B-tree key rather
446                // than at the start of `index_rows`, so a seek-initiated scan does
447                // not re-walk every preceding version on its first row check.
448                let iter_box: Box<
449                    dyn Iterator<Item = MvccEntry<'_, Arc<SortableIndexKey>, A>> + Send + Sync,
450                > = match index_rows {
451                    Some(index_rows) => {
452                        Box::new(index_rows.value().range::<SortableIndexKey, _>((
453                            std::ops::Bound::Included(key.as_ref()),
454                            std::ops::Bound::Unbounded,
455                        )))
456                    }
457                    None => Box::new(std::iter::empty()),
458                };
459                static_iterator_hack!(iter_box, Arc<SortableIndexKey>, A)
460            };
461            *self = Self::advance(iter);
462        }
463        loop {
464            match self {
465                // No version at or after this key -> B-tree row is visible.
466                Self::Exhausted => return true,
467                Self::Uninitialized => unreachable!("created just above"),
468                Self::Peeked {
469                    key: finger_key,
470                    versions,
471                    ..
472                } => match finger_key.as_ref().cmp(key.as_ref()) {
473                    // No version exactly at this key -> visible.
474                    std::cmp::Ordering::Greater => return true,
475                    // Version present at this key -> resolve the shadow bit now,
476                    // on the one key that actually matches a B-tree row.
477                    std::cmp::Ordering::Equal => {
478                        return !db.index_chain_invalidates_btree(versions, tx_id);
479                    }
480                    // Finger behind the B-tree (a version-only key); catch up below.
481                    std::cmp::Ordering::Less => {}
482                },
483            }
484            // Step the finger forward; only the `Less` arm above falls through here.
485            let Self::Peeked { iter, .. } = std::mem::replace(self, Self::Uninitialized) else {
486                unreachable!("Less arm matched Peeked")
487            };
488            *self = Self::advance(iter);
489        }
490    }
491}
492
493pub struct MvccLazyCursor<Clock: LogicalClock + 'static, A: ConcurrentAllocator = TursoAllocator> {
494    pub db: Arc<MvStore<Clock, A>>,
495    #[cfg(any(clt_turso_tests, injected_yields))]
496    connection: Arc<Connection>,
497    #[cfg(any(clt_turso_tests, injected_yields))]
498    yield_instance_id: u64,
499    current_pos: CursorPosition<A>,
500    /// Stateful MVCC table iterator if this is a table cursor.
501    table_iterator: Option<MvccIterator<'static, RowID, A>>,
502    /// Stateful MVCC index iterator if this is an index cursor.
503    index_iterator: Option<MvccIterator<'static, Arc<SortableIndexKey>, A>>,
504    mv_cursor_type: MvccCursorType,
505    table_id: MVTableId,
506    tx_id: u64,
507    /// Reusable immutable record, used to allow better allocation strategy.
508    reusable_immutable_record: Option<ImmutableRecord>,
509    btree_cursor: Box<dyn CursorTrait>,
510    null_flag: bool,
511    creating_new_rowid: bool,
512    state: Option<MvccLazyCursorState>,
513    // we keep count_state separate to be able to call other public functions like rewind and next
514    count_state: Option<CountState>,
515    btree_advance_state: Option<AdvanceBtreeState>,
516    /// Dual-cursor peek state for proper iteration
517    dual_peek: DualCursorPeek<A>,
518    /// Forward-scan finger over `index_rows`; see [`IndexShadowFinger`].
519    index_finger: IndexShadowFinger<A>,
520    /// [`MvStore::index_rows_epoch`] snapshot taken the last time
521    /// `index_finger` was consulted. New index keys can be created at or
522    /// behind an already-positioned finger while the scan's cursor is open
523    /// (e.g. a DELETE on the same connection inserts a tombstone key
524    /// mid-scan, #7578); versions appended to *existing* keys are fine
525    /// (chains are read live through their `Arc`), but a new key would be
526    /// silently skipped. On an epoch mismatch the finger is reset so it
527    /// reseeds at the current B-tree key instead of trusting its stale
528    /// position.
529    index_finger_epoch: u64,
530}
531
532pub enum NextRowidResult {
533    /// We need to go to the last rowid and intialize allocator
534    Uninitialized,
535    /// It was initialized, so we get a new rowid
536    Next {
537        new_rowid: i64,
538        prev_rowid: Option<i64>,
539    },
540    /// We reached end of available rowids (i64::MAX), so we will have to try and find a random rowid.
541    FindRandom,
542}
543
544impl<Clock: LogicalClock + 'static, A: ConcurrentAllocator> MvccLazyCursor<Clock, A> {
545    pub fn new(
546        db: Arc<MvStore<Clock, A>>,
547        connection: &Arc<Connection>,
548        tx_id: u64,
549        root_page_or_table_id: i64,
550        mv_cursor_type: MvccCursorType,
551        btree_cursor: Box<dyn CursorTrait>,
552    ) -> Result<MvccLazyCursor<Clock, A>> {
553        turso_assert!(
554            (&*btree_cursor as &dyn Any).is::<BTreeCursor>(),
555            "BTreeCursor expected for mvcc cursor"
556        );
557        // Resolve the root page against this reader's snapshot: a PASSIVE checkpoint may have
558        // dropped (and possibly reused) the page during collection while we still reference it at an
559        // older snapshot. The WAL read mark keeps the pages readable; this keeps the in-memory
560        // root_page -> table_id reverse lookup snapshot-consistent. See `retired_rootpages`.
561        let snapshot_ts = db.read_snapshot_ts(tx_id);
562        let table_id = if connection.experimental_mvcc_passive_checkpoint_enabled() {
563            // Under PASSIVE checkpointing a transaction can capture a schema cookie older than
564            // the drop committed within its own snapshot (the drop publishes its cookie after
565            // the transaction reads the header, even though the drop's commit ts precedes the
566            // transaction's begin ts). The compiled cursor then points at a positive root page
567            // its snapshot already sees dropped. That is a stale-schema read, not an invariant
568            // violation: reprepare against the current schema instead of panicking.
569            db.try_get_table_id_from_root_page_at(root_page_or_table_id, snapshot_ts)
570                .ok_or(LimboError::SchemaUpdated)?
571        } else {
572            db.get_table_id_from_root_page_at(root_page_or_table_id, snapshot_ts)
573        };
574        Ok(Self {
575            db,
576            #[cfg(any(clt_turso_tests, injected_yields))]
577            yield_instance_id: connection.next_yield_instance_id(),
578            #[cfg(any(clt_turso_tests, injected_yields))]
579            connection: connection.clone(),
580            tx_id,
581            table_iterator: None,
582            index_iterator: None,
583            mv_cursor_type,
584            current_pos: CursorPosition::BeforeFirst,
585            table_id,
586            reusable_immutable_record: None,
587            btree_cursor,
588            null_flag: false,
589            creating_new_rowid: false,
590            state: None,
591            count_state: None,
592            btree_advance_state: None,
593            dual_peek: DualCursorPeek::default(),
594            index_finger: IndexShadowFinger::default(),
595            index_finger_epoch: 0,
596        })
597    }
598
599    /// Forward-direction shadow check: finger fast-path for index cursors, the
600    /// authoritative per-row lookup for table cursors.
601    fn btree_row_is_valid_forward(&mut self, key: &RowKey) -> bool {
602        let RowKey::Record(rec) = key else {
603            return self.query_btree_version_is_valid(key);
604        };
605        // Read the epoch before the finger (re)seeds: if a key insert races
606        // past this load, the next shadow check observes the mismatch and
607        // resets. See `index_finger_epoch`.
608        let epoch = self.db.index_rows_epoch();
609        if self.index_finger_epoch != epoch {
610            self.index_finger.reset();
611            self.index_finger_epoch = epoch;
612        }
613        let valid = self
614            .index_finger
615            .btree_row_is_valid(&self.db, self.table_id, self.tx_id, rec);
616        // Debug-only cross-check: any finger divergence (e.g. a missed reset)
617        // fails the test suite instead of shipping.
618        #[cfg(debug_assertions)]
619        debug_assert_eq!(
620            valid,
621            self.db.query_btree_version_is_valid(
622                self.table_id,
623                &RowKey::Record(rec.clone()),
624                self.tx_id
625            ),
626            "index finger diverged from query_btree_version_is_valid"
627        );
628        valid
629    }
630
631    /// Returns the current row as an immutable record.
632    pub fn current_row(&mut self) -> Result<IOResult<Option<&crate::types::ImmutableRecord>>> {
633        if self.get_null_flag() {
634            return Ok(IOResult::Done(None));
635        }
636        tracing::trace!("current_row({:?})", self.current_pos);
637        match &self.current_pos {
638            CursorPosition::Loaded { in_btree: true, .. } => self.btree_cursor.record(),
639            CursorPosition::Loaded {
640                in_btree: false, ..
641            } => {
642                // Lightweight handle clone (refcount bump) so we can drop the
643                // borrow of `current_pos` and mutably borrow the reusable record.
644                let versions = match &self.current_pos {
645                    CursorPosition::Loaded { versions, .. } => versions.clone(),
646                    _ => unreachable!("matched Loaded above"),
647                };
648
649                let found = if let Some(versions) = &versions {
650                    // Fast path: serialize the visible version straight into our
651                    // reusable record — like the btree cursor does with a cell —
652                    // instead of cloning a `Row` first.
653                    if self.reusable_immutable_record.is_none() {
654                        self.reusable_immutable_record = Some(ImmutableRecord::new(1024)?);
655                    }
656                    let record = self.reusable_immutable_record.as_mut().unwrap();
657                    self.db
658                        .read_visible_into_record(self.tx_id, versions, record)?
659                } else {
660                    // Cold fallback (seek-positioned, no cached chain): point
661                    // lookup, then serialize.
662                    let row_id = match &self.current_pos {
663                        CursorPosition::Loaded { row_id, .. } => row_id.clone(),
664                        _ => unreachable!("matched Loaded above"),
665                    };
666                    let maybe_index_id = match &self.mv_cursor_type {
667                        MvccCursorType::Index(_) => Some(self.table_id),
668                        MvccCursorType::Table => None,
669                    };
670                    match self
671                        .db
672                        .read_from_table_or_index(self.tx_id, &row_id, maybe_index_id)?
673                    {
674                        Some(row) => {
675                            let record = self.get_immutable_record_or_create()?;
676                            record.invalidate();
677                            record.start_serialization(row.payload())?;
678                            true
679                        }
680                        None => false,
681                    }
682                };
683
684                if !found {
685                    return Ok(IOResult::Done(None));
686                }
687                let record_ref = self.reusable_immutable_record.as_ref().ok_or_else(|| {
688                    LimboError::InternalError("immutable record not initialized".to_string())
689                })?;
690                Ok(IOResult::Done(Some(record_ref)))
691            }
692            CursorPosition::BeforeFirst => {
693                // Before first is not a valid position, so we return none.
694                Ok(IOResult::Done(None))
695            }
696            CursorPosition::End => Ok(IOResult::Done(None)),
697        }
698    }
699
700    pub fn read_mvcc_current_row(&self) -> Result<Option<Row>> {
701        let (row_id, versions) = match &self.current_pos {
702            CursorPosition::Loaded {
703                row_id,
704                in_btree,
705                versions,
706            } if !in_btree => (row_id, versions),
707            _ => panic!("invalid position to read current mvcc row"),
708        };
709        // Scan path: the range iterator already resolved this row's version
710        // chain, so read it directly instead of a second skiplist lookup.
711        if let Some(versions) = versions {
712            return self.db.read_visible_from_versions(self.tx_id, versions);
713        }
714        let maybe_index_id = match &self.mv_cursor_type {
715            MvccCursorType::Index(_) => Some(self.table_id),
716            MvccCursorType::Table => None,
717        };
718        self.db
719            .read_from_table_or_index(self.tx_id, row_id, maybe_index_id)
720    }
721
722    pub fn close(self) -> Result<()> {
723        Ok(())
724    }
725
726    pub fn start_new_rowid(&mut self) -> Result<IOResult<NextRowidResult>> {
727        tracing::trace!("start_new_rowid");
728
729        let allocator = self.db.get_rowid_allocator(&self.table_id);
730        let locked = allocator.lock();
731        if !locked {
732            // Yield, some other cursor is generating new rowid
733            return Ok(IOResult::IO(IOCompletions::Single(Completion::new_yield())));
734        }
735
736        self.creating_new_rowid = true;
737        let res = if allocator.is_uninitialized() {
738            NextRowidResult::Uninitialized
739        } else if let Some((next_rowid, prev_max_rowid)) = allocator.get_next_rowid() {
740            NextRowidResult::Next {
741                new_rowid: next_rowid,
742                prev_rowid: prev_max_rowid,
743            }
744        } else {
745            NextRowidResult::FindRandom
746        };
747        Ok(IOResult::Done(res))
748    }
749
750    pub fn initialize_max_rowid(&mut self, max_rowid: Option<i64>) -> Result<()> {
751        let allocator = self.db.get_rowid_allocator(&self.table_id);
752        turso_assert!(
753            self.creating_new_rowid,
754            "cursor didn't start creating new rowid"
755        );
756        allocator.initialize(max_rowid);
757        Ok(())
758    }
759
760    /// Allocate the next rowid from the (already initialized) allocator.
761    /// Must be called while holding the allocator lock.
762    pub fn allocate_next_rowid(&self) -> Option<(i64, Option<i64>)> {
763        let allocator = self.db.get_rowid_allocator(&self.table_id);
764        allocator.get_next_rowid()
765    }
766
767    pub fn end_new_rowid(&mut self) {
768        tracing::trace!(
769            "end_new_rowid creating_new_rowid={}",
770            self.creating_new_rowid
771        );
772        // if we started creating a new rowid, we need to unlock the allocator
773        // this might be false if there was an error during `op_new_rowid` before calling `start_new_rowid` so we can call this function
774        // in any case
775        if self.creating_new_rowid {
776            let allocator = self.db.get_rowid_allocator(&self.table_id);
777            allocator.unlock();
778            self.creating_new_rowid = false;
779        }
780    }
781
782    fn get_immutable_record_or_create(&mut self) -> Result<&mut ImmutableRecord> {
783        if self.reusable_immutable_record.is_none() {
784            self.reusable_immutable_record = Some(ImmutableRecord::new(1024)?);
785        }
786        Ok(self.reusable_immutable_record.as_mut().unwrap())
787    }
788
789    fn get_current_pos(&self) -> CursorPosition<A> {
790        self.current_pos.clone()
791    }
792
793    fn is_btree_allocated(&self) -> bool {
794        // Dual gate (logical base-validity AND physical visibility): a PASSIVE checkpoint may
795        // materialize this object's btree during collection. This cursor may read it only if the binding
796        // covers our snapshot AND its pages were already durable when we pinned our read mark
797        // (`visible_from <= observed_boundary`). A cursor that opened before checkpoint publish
798        // materialization therefore stays version-store-only for its whole life and never seeks
799        // the page its read mark can't see. See `MvStore::is_btree_readable_at`.
800        let begin_ts = self.db.read_snapshot_ts(self.tx_id);
801        let read_mark = self.db.read_tx_mark(self.tx_id);
802        self.db
803            .is_btree_readable_at(&self.table_id, begin_ts, read_mark)
804    }
805
806    fn query_btree_version_is_valid(&self, key: &RowKey) -> bool {
807        self.db
808            .query_btree_version_is_valid(self.table_id, key, self.tx_id)
809    }
810
811    /// Advance MVCC iterator and return next visible row key in the direction that the iterator was initialized in.
812    fn advance_mvcc_iterator(&mut self) {
813        let new_peek_state = match &self.mv_cursor_type {
814            MvccCursorType::Table => match self.db.advance_cursor_and_get_row_id_for_table(
815                self.table_id,
816                &mut self.table_iterator,
817                self.tx_id,
818            ) {
819                Some((row_id, versions)) => CursorPeek::Row {
820                    key: row_id.row_id,
821                    versions: Some(versions),
822                },
823                None => CursorPeek::Exhausted,
824            },
825            MvccCursorType::Index(_) => match self
826                .db
827                .advance_cursor_and_get_row_id_for_index(&mut self.index_iterator, self.tx_id)
828            {
829                Some(row_id) => CursorPeek::Row {
830                    key: row_id.row_id,
831                    versions: None,
832                },
833                None => CursorPeek::Exhausted,
834            },
835        };
836        self.dual_peek.mvcc_peek = new_peek_state;
837    }
838
839    /// Advance btree cursor forward and set btree peek to the first valid row key (skipping rows shadowed by MVCC)
840    fn advance_btree_forward(&mut self) -> Result<IOResult<()>> {
841        self._advance_btree_forward(true)
842    }
843
844    /// Advance btree cursor forward from current position (cursor already positioned by seek)
845    fn advance_btree_forward_from_current(&mut self) -> Result<IOResult<()>> {
846        self._advance_btree_forward(false)
847    }
848
849    fn _advance_btree_forward(&mut self, initialize: bool) -> Result<IOResult<()>> {
850        loop {
851            let state = self.btree_advance_state;
852            match state {
853                None => {
854                    if !self.is_btree_allocated() {
855                        self.dual_peek.btree_peek = CursorPeek::Exhausted;
856                        self.btree_advance_state = None;
857                        return Ok(IOResult::Done(()));
858                    }
859                    // If the btree is uninitialized AND we should initialize, do the equivalent of rewind() to find the first valid row
860                    if initialize && self.dual_peek.btree_uninitialized() {
861                        return_if_io!(self.btree_cursor.rewind());
862                        self.btree_advance_state = Some(AdvanceBtreeState::RewindCheckBtreeKey);
863                    } else {
864                        self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
865                    }
866                    inject_io_yield!(self, CursorYieldPoint::AdvanceBtreeForwardProgress);
867                }
868                Some(AdvanceBtreeState::RewindCheckBtreeKey) => {
869                    let key = self.get_btree_current_key()?;
870                    match key {
871                        Some(k) if self.btree_row_is_valid_forward(&k) => {
872                            self.dual_peek.btree_peek = CursorPeek::Row {
873                                key: k,
874                                versions: None,
875                            };
876                            self.btree_advance_state = None;
877                            return Ok(IOResult::Done(()));
878                        }
879                        Some(_) => {
880                            // shadowed by MVCC, continue to next
881                            self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
882                        }
883                        None => {
884                            self.dual_peek.btree_peek = CursorPeek::Exhausted;
885                            self.btree_advance_state = None;
886                            return Ok(IOResult::Done(()));
887                        }
888                    }
889                }
890                Some(AdvanceBtreeState::NextBtree) => {
891                    let peek = &mut self.dual_peek;
892                    return_if_io!(self.btree_cursor.next());
893                    let found = self.btree_cursor.has_record();
894                    if !found {
895                        peek.btree_peek = CursorPeek::Exhausted;
896                        self.btree_advance_state = None;
897                        return Ok(IOResult::Done(()));
898                    }
899                    self.btree_advance_state = Some(AdvanceBtreeState::NextCheckBtreeKey);
900                    inject_io_yield!(self, CursorYieldPoint::AdvanceBtreeForwardProgress);
901                }
902                Some(AdvanceBtreeState::NextCheckBtreeKey) => {
903                    let key = self.get_btree_current_key()?;
904                    if let Some(key) = key {
905                        if self.btree_row_is_valid_forward(&key) {
906                            self.dual_peek.btree_peek = CursorPeek::Row {
907                                key,
908                                versions: None,
909                            };
910                            self.btree_advance_state = None;
911                            return Ok(IOResult::Done(()));
912                        }
913                        // Row is shadowed by MVCC, continue to next
914                        // FIXME: do we want to iterate over all shadowed rows? If every row is shadowed by MVCC, we will iterate the whole btree in a single `next` call
915                        self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
916                    } else {
917                        self.dual_peek.btree_peek = CursorPeek::Exhausted;
918                        self.btree_advance_state = None;
919                        return Ok(IOResult::Done(()));
920                    }
921                }
922            }
923        }
924    }
925
926    /// Advance btree cursor backward and set btree peek to the first valid row key (skipping rows shadowed by MVCC)
927    fn advance_btree_backward(&mut self) -> Result<IOResult<()>> {
928        self._advance_btree_backward(true)
929    }
930
931    /// Advance btree cursor backward from current position (cursor already positioned by seek)
932    fn advance_btree_backward_from_current(&mut self) -> Result<IOResult<()>> {
933        self._advance_btree_backward(false)
934    }
935
936    fn _advance_btree_backward(&mut self, initialize: bool) -> Result<IOResult<()>> {
937        loop {
938            let state = self.btree_advance_state;
939            match state {
940                None => {
941                    if !self.is_btree_allocated() {
942                        let peek = &mut self.dual_peek;
943                        peek.btree_peek = CursorPeek::Exhausted;
944                        self.btree_advance_state = None;
945                        return Ok(IOResult::Done(()));
946                    }
947                    // If the btree is uninitialized AND we should initialize, do the equivalent of last() to find the last valid row
948                    if initialize && self.dual_peek.btree_uninitialized() {
949                        return_if_io!(self.btree_cursor.last());
950                        self.btree_advance_state = Some(AdvanceBtreeState::RewindCheckBtreeKey);
951                    } else {
952                        self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
953                    }
954                    inject_io_yield!(self, CursorYieldPoint::AdvanceBtreeBackwardProgress);
955                }
956                Some(AdvanceBtreeState::RewindCheckBtreeKey) => {
957                    let key = self.get_btree_current_key()?;
958                    match key {
959                        Some(k) if self.query_btree_version_is_valid(&k) => {
960                            self.dual_peek.btree_peek = CursorPeek::Row {
961                                key: k,
962                                versions: None,
963                            };
964                            self.btree_advance_state = None;
965                            return Ok(IOResult::Done(()));
966                        }
967                        Some(_) => {
968                            // shadowed by MVCC, continue to prev
969                            self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
970                        }
971                        None => {
972                            self.dual_peek.btree_peek = CursorPeek::Exhausted;
973                            self.btree_advance_state = None;
974                            return Ok(IOResult::Done(()));
975                        }
976                    }
977                }
978                Some(AdvanceBtreeState::NextBtree) => {
979                    return_if_io!(self.btree_cursor.prev());
980                    let peek = &mut self.dual_peek;
981                    let found = self.btree_cursor.has_record();
982                    if !found {
983                        peek.btree_peek = CursorPeek::Exhausted;
984                        self.btree_advance_state = None;
985                        return Ok(IOResult::Done(()));
986                    }
987                    self.btree_advance_state = Some(AdvanceBtreeState::NextCheckBtreeKey);
988                    inject_io_yield!(self, CursorYieldPoint::AdvanceBtreeBackwardProgress);
989                }
990                Some(AdvanceBtreeState::NextCheckBtreeKey) => {
991                    let key = self.get_btree_current_key()?;
992                    match key {
993                        Some(k) if self.query_btree_version_is_valid(&k) => {
994                            self.dual_peek.btree_peek = CursorPeek::Row {
995                                key: k,
996                                versions: None,
997                            };
998                            self.btree_advance_state = None;
999                            return Ok(IOResult::Done(()));
1000                        }
1001                        Some(_) => {
1002                            // shadowed by MVCC, continue to prev
1003                            self.btree_advance_state = Some(AdvanceBtreeState::NextBtree);
1004                        }
1005                        None => {
1006                            self.dual_peek.btree_peek = CursorPeek::Exhausted;
1007                            self.btree_advance_state = None;
1008                            return Ok(IOResult::Done(()));
1009                        }
1010                    }
1011                }
1012            }
1013        }
1014    }
1015
1016    /// Get the current key from btree cursor
1017    fn get_btree_current_key(&mut self) -> Result<Option<RowKey>> {
1018        match &self.mv_cursor_type {
1019            MvccCursorType::Table => {
1020                let maybe_rowid = loop {
1021                    match self.btree_cursor.rowid()? {
1022                        IOResult::Done(maybe_rowid) => {
1023                            break maybe_rowid.map(RowKey::Int);
1024                        }
1025                        IOResult::IO(c) => {
1026                            c.wait(self.btree_cursor.get_pager().io.as_ref())?; // FIXME: sync IO hack
1027                        }
1028                    }
1029                };
1030                Ok(maybe_rowid)
1031            }
1032            MvccCursorType::Index(index_info) => {
1033                let maybe_record = loop {
1034                    match self.btree_cursor.record()? {
1035                        IOResult::Done(maybe_record) => {
1036                            break maybe_record;
1037                        }
1038                        IOResult::IO(c) => {
1039                            c.wait(self.btree_cursor.get_pager().io.as_ref())?; // FIXME: sync IO hack
1040                        }
1041                    }
1042                };
1043                Ok(maybe_record.map(|record| {
1044                    RowKey::Record(Arc::new(SortableIndexKey {
1045                        key: record.clone(),
1046                        metadata: index_info.clone(),
1047                    }))
1048                }))
1049            }
1050        }
1051    }
1052
1053    /// Refresh the current position based on the peek values
1054    fn refresh_current_position(&mut self, dir: IterationDirection) {
1055        let new_position = self.dual_peek.cursor_position_from_next(self.table_id, dir);
1056        self.current_pos = new_position;
1057    }
1058
1059    /// Reset dual peek state (called on rewind/last/seek)
1060    fn reset_dual_peek(&mut self) {
1061        self.dual_peek = DualCursorPeek::default();
1062        // The forward finger is monotonic; a reposition invalidates it.
1063        self.index_finger.reset();
1064    }
1065
1066    /// Seek btree cursor and set btree_peek to the result.
1067    /// Skips rows that are shadowed by MVCC.
1068    /// Returns IOResult indicating if we need to yield for IO or are done.
1069    fn seek_btree_and_set_peek(
1070        &mut self,
1071        seek_key: SeekKey<'_>,
1072        op: SeekOp,
1073    ) -> Result<IOResult<()>> {
1074        // Fast path: btree not allocated
1075        if !self.is_btree_allocated() {
1076            self.dual_peek.btree_peek = CursorPeek::Exhausted;
1077            self.state = None;
1078            return Ok(IOResult::Done(()));
1079        }
1080
1081        loop {
1082            let Some(MvccLazyCursorState::Seek(SeekState::SeekBtree(btree_seek_state), direction)) =
1083                self.state.clone()
1084            else {
1085                panic!(
1086                    "Invalid btree seek state in seek_btree_and_set_peek: {:?}",
1087                    self.state
1088                );
1089            };
1090            match btree_seek_state {
1091                SeekBtreeState::SeekBtree => {
1092                    let seek_result = return_if_io!(self.btree_cursor.seek(seek_key.clone(), op));
1093
1094                    match seek_result {
1095                        SeekResult::NotFound => {
1096                            self.dual_peek.btree_peek = CursorPeek::Exhausted;
1097                            return Ok(IOResult::Done(()));
1098                        }
1099                        SeekResult::TryAdvance => {
1100                            // Need to advance to find actual matching entry
1101                            self.state.replace(MvccLazyCursorState::Seek(
1102                                SeekState::SeekBtree(SeekBtreeState::AdvanceBTree),
1103                                direction,
1104                            ));
1105                            inject_io_yield!(self, CursorYieldPoint::SeekBtreeProgress);
1106                        }
1107                        SeekResult::Found => {
1108                            self.state.replace(MvccLazyCursorState::Seek(
1109                                SeekState::SeekBtree(SeekBtreeState::CheckRow),
1110                                direction,
1111                            ));
1112                            inject_io_yield!(self, CursorYieldPoint::SeekBtreeProgress);
1113                        }
1114                    }
1115                }
1116                SeekBtreeState::AdvanceBTree => {
1117                    return_if_io!(match direction {
1118                        IterationDirection::Forwards => {
1119                            self.advance_btree_forward_from_current()
1120                        }
1121                        IterationDirection::Backwards => {
1122                            self.advance_btree_backward_from_current()
1123                        }
1124                    });
1125                    self.state.replace(MvccLazyCursorState::Seek(
1126                        SeekState::SeekBtree(SeekBtreeState::CheckRow),
1127                        direction,
1128                    ));
1129                    inject_io_yield!(self, CursorYieldPoint::SeekBtreeProgress);
1130                }
1131                SeekBtreeState::CheckRow => {
1132                    let key = self.get_btree_current_key()?;
1133                    match key {
1134                        Some(k) if self.query_btree_version_is_valid(&k) => {
1135                            self.dual_peek.btree_peek = CursorPeek::Row {
1136                                key: k,
1137                                versions: None,
1138                            };
1139                            return Ok(IOResult::Done(()));
1140                        }
1141                        Some(_) => {
1142                            // shadowed by MVCC, continue to next
1143                            self.state.replace(MvccLazyCursorState::Seek(
1144                                SeekState::SeekBtree(SeekBtreeState::AdvanceBTree),
1145                                direction,
1146                            ));
1147                            inject_io_yield!(self, CursorYieldPoint::SeekBtreeProgress);
1148                        }
1149                        None => {
1150                            self.dual_peek.btree_peek = CursorPeek::Exhausted;
1151                            return Ok(IOResult::Done(()));
1152                        }
1153                    }
1154                }
1155            }
1156        }
1157    }
1158
1159    /// Initialize MVCC iterator for forward iteration (used when next() is called without rewind())
1160    fn init_mvcc_iterator_forward(&mut self) -> Result<(), TryReserveError> {
1161        if self.table_iterator.is_some() || self.index_iterator.is_some() {
1162            return Ok(()); // Already initialized
1163        }
1164        match &self.mv_cursor_type {
1165            MvccCursorType::Table => {
1166                let start_rowid = RowID {
1167                    table_id: self.table_id,
1168                    row_id: RowKey::Int(i64::MIN),
1169                };
1170                let range =
1171                    create_seek_range(Bound::Included(start_rowid), IterationDirection::Forwards);
1172                let iter_box = Box::new(self.db.rows.range(range));
1173                self.table_iterator = Some(static_iterator_hack!(iter_box, RowID, A));
1174            }
1175            MvccCursorType::Index(_) => {
1176                let index_rows = self.db.get_or_create_index_rows(self.table_id)?;
1177                let index_rows = index_rows.value();
1178                let iter_box: Box<
1179                    dyn Iterator<Item = MvccEntry<'_, Arc<SortableIndexKey>, A>> + Send + Sync,
1180                > = Box::new(index_rows.iter());
1181                self.index_iterator =
1182                    Some(static_iterator_hack!(iter_box, Arc<SortableIndexKey>, A));
1183            }
1184        }
1185        Ok(())
1186    }
1187}
1188
1189impl<Clock: LogicalClock + 'static, A: ConcurrentAllocator> Drop for MvccLazyCursor<Clock, A> {
1190    fn drop(&mut self) {
1191        // Release the per-table RowidAllocator lock if a Statement was dropped
1192        // while paused at an op_new_rowid IO yield. end_new_rowid is a no-op
1193        // when creating_new_rowid is false, so this is safe in every case.
1194        self.end_new_rowid();
1195    }
1196}
1197
1198impl<Clock: LogicalClock + 'static, A: ConcurrentAllocator> CursorTrait
1199    for MvccLazyCursor<Clock, A>
1200{
1201    fn last(&mut self) -> Result<IOResult<()>> {
1202        // A cursor may be NullRow'd during outer-join unmatched emission.
1203        // Repositioning to a real row must clear that synthetic NULL state.
1204        self.set_null_flag(false);
1205        let state = self.state.clone();
1206        if state.is_none() {
1207            let _ = self.table_iterator.take();
1208            let _ = self.index_iterator.take();
1209            self.reset_dual_peek();
1210            self.state
1211                .replace(MvccLazyCursorState::Rewind(RewindState::Advance));
1212        }
1213
1214        turso_assert!(
1215            matches!(
1216                self.state
1217                    .as_ref()
1218                    .expect("rewind state is not initialized"),
1219                MvccLazyCursorState::Rewind(RewindState::Advance)
1220            ),
1221            "invalid last state",
1222            { "state": format!("{:?}", self.state) }
1223        );
1224
1225        // Initialize btree cursor to last position
1226        return_if_io!(self.advance_btree_backward());
1227
1228        self.invalidate_record();
1229        self.current_pos = CursorPosition::End;
1230
1231        // Initialize MVCC iterator to last position
1232        match &self.mv_cursor_type {
1233            MvccCursorType::Table => match self.db.get_last_table_rowid(
1234                self.table_id,
1235                &mut self.table_iterator,
1236                self.tx_id,
1237            ) {
1238                Some(k) => {
1239                    tracing::trace!("last: mvcc_key: {:?}", k);
1240                    self.dual_peek.mvcc_peek = CursorPeek::Row {
1241                        key: k,
1242                        versions: None,
1243                    };
1244                }
1245                None => {
1246                    self.dual_peek.mvcc_peek = CursorPeek::Exhausted;
1247                }
1248            },
1249            MvccCursorType::Index(_) => match self.db.get_last_index_rowid(
1250                self.table_id,
1251                self.tx_id,
1252                &mut self.index_iterator,
1253            )? {
1254                Some(k) => {
1255                    self.dual_peek.mvcc_peek = CursorPeek::Row {
1256                        key: k,
1257                        versions: None,
1258                    };
1259                }
1260                None => {
1261                    self.dual_peek.mvcc_peek = CursorPeek::Exhausted;
1262                }
1263            },
1264        };
1265
1266        self.refresh_current_position(IterationDirection::Backwards);
1267        self.invalidate_record();
1268        self.state = None;
1269
1270        Ok(IOResult::Done(()))
1271    }
1272
1273    /// Move the cursor to the next row. Returns true if the cursor moved to the next row, false if the cursor is at the end of the table.
1274    ///
1275    /// Uses dual-cursor approach: only advances the cursor that was just consumed.
1276    fn next(&mut self) -> Result<IOResult<()>> {
1277        if self.state.is_none() {
1278            // If BeforeFirst and peek not initialized, initialize the iterators and peek values
1279            let current_pos = self.get_current_pos();
1280            if matches!(current_pos, CursorPosition::BeforeFirst) {
1281                let uninitialized = self.dual_peek.both_uninitialized();
1282                if uninitialized {
1283                    // Initialize MVCC iterator and get first peek
1284                    self.init_mvcc_iterator_forward()?;
1285                    self.advance_mvcc_iterator();
1286                    self.state
1287                        .replace(MvccLazyCursorState::Next(NextState::AdvanceUnitialized));
1288                } else {
1289                    self.state
1290                        .replace(MvccLazyCursorState::Next(NextState::CheckNeedsAdvance));
1291                }
1292                inject_io_yield!(self, CursorYieldPoint::NextStart);
1293            } else {
1294                self.state
1295                    .replace(MvccLazyCursorState::Next(NextState::CheckNeedsAdvance));
1296                inject_io_yield!(self, CursorYieldPoint::NextStart);
1297            }
1298        }
1299        // If it was uninitialized, we need to advance the btree first
1300        if matches!(
1301            self.state.as_ref().expect("next state is not initialized"),
1302            MvccLazyCursorState::Next(NextState::AdvanceUnitialized)
1303        ) {
1304            return_if_io!(self.advance_btree_forward());
1305            self.state
1306                .replace(MvccLazyCursorState::Next(NextState::CheckNeedsAdvance));
1307        }
1308
1309        if matches!(
1310            self.state.as_ref().expect("next state is not initialized"),
1311            MvccLazyCursorState::Next(NextState::CheckNeedsAdvance)
1312        ) {
1313            // Determine which cursor(s) need to be advanced based on current position
1314            let current_pos = self.get_current_pos();
1315            let (need_advance_mvcc, need_advance_btree) = match &current_pos {
1316                CursorPosition::BeforeFirst => {
1317                    // First call after rewind - peek values should already be populated
1318                    // Just need to pick the smaller one
1319                    (false, false)
1320                }
1321                CursorPosition::Loaded { in_btree, .. } => {
1322                    // Advance whichever cursor we just consumed
1323                    if *in_btree {
1324                        (false, true) // Last row was from btree, advance btree
1325                    } else {
1326                        (true, false) // Last row was from MVCC, advance MVCC
1327                    }
1328                }
1329                CursorPosition::End => {
1330                    self.state = None;
1331                    return Ok(IOResult::Done(()));
1332                }
1333            };
1334
1335            // Advance cursors as needed and update peek state
1336            if need_advance_mvcc && !self.dual_peek.mvcc_exhausted() {
1337                self.advance_mvcc_iterator();
1338            }
1339            if need_advance_btree && !self.dual_peek.btree_exhausted() {
1340                self.state
1341                    .replace(MvccLazyCursorState::Next(NextState::Advance));
1342                inject_io_yield!(self, CursorYieldPoint::NextBtreeAdvance);
1343            }
1344        }
1345
1346        if matches!(
1347            self.state.as_ref().expect("next state is not initialized"),
1348            MvccLazyCursorState::Next(NextState::Advance)
1349        ) {
1350            return_if_io!(self.advance_btree_forward());
1351        }
1352
1353        self.refresh_current_position(IterationDirection::Forwards);
1354        self.invalidate_record();
1355        self.state = None;
1356
1357        Ok(IOResult::Done(()))
1358    }
1359
1360    /// Move the cursor to the previous row. Returns true if the cursor moved, false if at the beginning.
1361    ///
1362    /// Uses dual-cursor approach: only advances the cursor that was just consumed.
1363    fn prev(&mut self) -> Result<IOResult<()>> {
1364        if self.state.is_none() {
1365            // If End and peek not initialized, initialize via last()
1366            let current_pos = self.get_current_pos();
1367            if matches!(current_pos, CursorPosition::End) {
1368                let uninitialized = self.dual_peek.both_uninitialized();
1369                if uninitialized {
1370                    self.state
1371                        .replace(MvccLazyCursorState::Prev(PrevState::AdvanceUnitialized));
1372                    return_if_io!(self.last());
1373                } else {
1374                    self.state
1375                        .replace(MvccLazyCursorState::Prev(PrevState::CheckNeedsAdvance));
1376                }
1377            } else {
1378                self.state
1379                    .replace(MvccLazyCursorState::Prev(PrevState::CheckNeedsAdvance));
1380            }
1381        }
1382
1383        if matches!(
1384            self.state.as_ref().expect("prev state is not initialized"),
1385            MvccLazyCursorState::Prev(PrevState::AdvanceUnitialized)
1386        ) {
1387            return_if_io!(self.last());
1388            self.state
1389                .replace(MvccLazyCursorState::Prev(PrevState::CheckNeedsAdvance));
1390        }
1391
1392        if matches!(
1393            self.state.as_ref().expect("prev state is not initialized"),
1394            MvccLazyCursorState::Prev(PrevState::CheckNeedsAdvance)
1395        ) {
1396            // Determine which cursor(s) need to be advanced based on current position
1397            let current_pos = self.get_current_pos();
1398            let (need_advance_mvcc, need_advance_btree) = match &current_pos {
1399                CursorPosition::End => {
1400                    // First call after last() - peek values should already be populated
1401                    (false, false)
1402                }
1403                CursorPosition::Loaded { in_btree, .. } => {
1404                    // Advance whichever cursor we just consumed
1405                    if *in_btree {
1406                        (false, true) // Last row was from btree, advance btree
1407                    } else {
1408                        (true, false) // Last row was from MVCC, advance MVCC
1409                    }
1410                }
1411                CursorPosition::BeforeFirst => {
1412                    self.state = None;
1413                    return Ok(IOResult::Done(()));
1414                }
1415            };
1416
1417            // Advance cursors as needed and update peek state
1418            if need_advance_mvcc && !self.dual_peek.mvcc_exhausted() {
1419                self.advance_mvcc_iterator();
1420            }
1421            if need_advance_btree && !self.dual_peek.btree_exhausted() {
1422                self.state
1423                    .replace(MvccLazyCursorState::Prev(PrevState::Advance));
1424                inject_io_yield!(self, CursorYieldPoint::PrevBtreeAdvance);
1425            }
1426        }
1427
1428        if matches!(
1429            self.state.as_ref().expect("prev state is not initialized"),
1430            MvccLazyCursorState::Prev(PrevState::Advance)
1431        ) {
1432            return_if_io!(self.advance_btree_backward());
1433        }
1434        self.refresh_current_position(IterationDirection::Backwards);
1435        self.invalidate_record();
1436        self.state = None;
1437
1438        Ok(IOResult::Done(()))
1439    }
1440
1441    fn rowid(&mut self) -> Result<IOResult<Option<i64>>> {
1442        if self.get_null_flag() {
1443            return Ok(IOResult::Done(None));
1444        }
1445        let rowid = match self.get_current_pos() {
1446            CursorPosition::Loaded {
1447                row_id,
1448                in_btree: _,
1449                ..
1450            } => match &row_id.row_id {
1451                RowKey::Int(id) => Some(*id),
1452                RowKey::Record(sortable_key) => {
1453                    // For index cursors, the rowid is stored in the last column of the index record
1454                    let MvccCursorType::Index(index_info) = &self.mv_cursor_type else {
1455                        panic!("RowKey::Record requires Index cursor type");
1456                    };
1457                    if index_info.has_rowid {
1458                        match sortable_key.key.last_value() {
1459                            Some(Ok(crate::types::ValueRef::Numeric(
1460                                crate::numeric::Numeric::Integer(rowid),
1461                            ))) => Some(rowid),
1462                            _ => {
1463                                crate::bail_parse_error!("Failed to parse rowid from index record")
1464                            }
1465                        }
1466                    } else {
1467                        crate::bail_parse_error!("Indexes without rowid are not supported in MVCC");
1468                    }
1469                }
1470            },
1471            CursorPosition::BeforeFirst => None,
1472            CursorPosition::End => None,
1473        };
1474        Ok(IOResult::Done(rowid))
1475    }
1476
1477    fn record(&mut self) -> Result<IOResult<Option<&crate::types::ImmutableRecord>>> {
1478        self.current_row()
1479    }
1480
1481    fn seek_unpacked(
1482        &mut self,
1483        registers: &[Register],
1484        op: SeekOp,
1485    ) -> Result<IOResult<SeekResult>> {
1486        let record = make_record(registers, &0, &registers.len())?;
1487        self.seek(SeekKey::IndexKey(&record), op)
1488    }
1489
1490    fn seek(&mut self, seek_key: SeekKey<'_>, op: SeekOp) -> Result<IOResult<SeekResult>> {
1491        // gt -> lower_bound bound excluded, we want first row after row_id
1492        // ge -> lower_bound bound included, we want first row equal to row_id or first row after row_id
1493        // lt -> upper_bound bound excluded, we want last row before row_id
1494        // le -> upper_bound bound included, we want last row equal to row_id or first row before row_id
1495
1496        // Skip the seek and short-circuit to SeekResult::Found if the following are true:
1497        //
1498        // - the seek is eq_only
1499        // - the cursor is already correctly positioned on a visible version
1500        //
1501        // This is because in the situation where the following are true:
1502        //
1503        // - the loop's seek is a range seek (not eq_only, ex: `DELETE ... WHERE a > 1000`)
1504        // - the seek_key for the current iteration is in MvStore, but not in the b-tree
1505        // - some matching rows are b-tree-resident. This can happen if there are inserts, then a
1506        //   checkpoint (moving all previous rows to the b-tree), and then more inserts (only in MvStore).
1507        //
1508        // then the following problem could happen:
1509        //
1510        // 1. we seek to the first matching key using `SeekOp::GT { eq_only: false }`, so far so good.
1511        // 2. op_idx_delete forces a eq_only seek on the cursor.
1512        //    In the case of a delete using an index, this is redundant,
1513        //    because the delete loop works by seeking the index and then Insn::DeferredSeek'ing the
1514        //    table, so the index cursor is already correctly positioned.
1515        // 3. we seek the mvcc cursor (self) and find the row
1516        // 4. we seek btree_cursor, don't find the row, and set it to Exhausted immediately because
1517        //    it's an eq_only seek, EVEN THOUGH the seek from step 1 would still have matched rows
1518        //    in the b-tree.
1519        // 5. eventually, the mvcc cursor runs out. When this happens, since btree_cursor is already
1520        //    exhausted, current_pos becomes CursorPosition::End, and the next Insn::Next
1521        //    INCORRECTLY finds the index cursor exhausted and breaks out of the delete loop, even
1522        //    though there are still b-tree-resident rows to delete.
1523        if self.state.is_none() && op.eq_only() {
1524            if let CursorPosition::Loaded {
1525                row_id, in_btree, ..
1526            } = &self.current_pos
1527            {
1528                if current_pos_matches_seek_key(&row_id.row_id, &seek_key, &self.mv_cursor_type)? {
1529                    let maybe_index_id = match &self.mv_cursor_type {
1530                        MvccCursorType::Index(_) => Some(self.table_id),
1531                        MvccCursorType::Table => None,
1532                    };
1533                    // The current row is visible either because MvStore has a visible version
1534                    // for it, or because it is a b-tree-resident row that is not shadowed by
1535                    // any MVCC version. Both cases must short-circuit: otherwise a b-tree-only
1536                    // row would fall through to the full eq-only seek below, which resets the
1537                    // iterators and marks the MVCC peek exhausted, skipping MvStore-resident
1538                    // rows that the enclosing range scan (see the comment above) still needs
1539                    // to visit.
1540                    let visible = self
1541                        .db
1542                        .read_from_table_or_index(self.tx_id, row_id, maybe_index_id)?
1543                        .is_some()
1544                        || (*in_btree && self.query_btree_version_is_valid(&row_id.row_id));
1545                    if visible {
1546                        // We need to clear the null flag for the table cursor before seeking,
1547                        // because it might have been set to false by an unmatched left-join row
1548                        // during the previous iteration on the outer loop.
1549                        self.set_null_flag(false);
1550                        return Ok(IOResult::Done(SeekResult::Found));
1551                    }
1552                }
1553            }
1554        }
1555
1556        loop {
1557            let state = self.state.clone();
1558            match state {
1559                None => {
1560                    // Initial state: Reset and do MVCC seek
1561                    let _ = self.table_iterator.take();
1562                    let _ = self.index_iterator.take();
1563                    self.reset_dual_peek();
1564                    self.invalidate_record();
1565                    // We need to clear the null flag for the table cursor before seeking,
1566                    // because it might have been set to false by an unmatched left-join row
1567                    // during the previous iteration on the outer loop.
1568                    self.set_null_flag(false);
1569
1570                    let direction = op.iteration_direction();
1571                    let inclusive = matches!(op, SeekOp::GE { .. } | SeekOp::LE { .. });
1572
1573                    match &seek_key {
1574                        SeekKey::TableRowId(row_id) => {
1575                            let rowid = RowID {
1576                                table_id: self.table_id,
1577                                row_id: RowKey::Int(*row_id),
1578                            };
1579
1580                            // Seek in MVCC (synchronous)
1581                            let mvcc_rowid = self.db.seek_rowid(
1582                                rowid.clone(),
1583                                inclusive,
1584                                op.eq_only(),
1585                                direction,
1586                                self.tx_id,
1587                                &mut self.table_iterator,
1588                            );
1589
1590                            // Set MVCC peek
1591                            {
1592                                self.dual_peek.mvcc_peek = match &mvcc_rowid {
1593                                    Some(rid) => CursorPeek::Row {
1594                                        key: rid.row_id.clone(),
1595                                        versions: None,
1596                                    },
1597                                    None => CursorPeek::Exhausted,
1598                                };
1599                            }
1600                        }
1601                        SeekKey::IndexKey(index_key) => {
1602                            let index_info = {
1603                                let MvccCursorType::Index(index_info) = &self.mv_cursor_type else {
1604                                    panic!("SeekKey::IndexKey requires Index cursor type");
1605                                };
1606                                Arc::new(IndexInfo::new_in(
1607                                    index_info.key_info.iter().cloned(),
1608                                    index_info.has_rowid,
1609                                    index_key.column_count(),
1610                                    index_info.is_unique,
1611                                    self.db.allocator(),
1612                                )?)
1613                            };
1614                            let sortable_key =
1615                                SortableIndexKey::new_from_record((*index_key).clone(), index_info);
1616
1617                            // Seek in MVCC (synchronous)
1618                            let mvcc_rowid = self.db.seek_index(
1619                                self.table_id,
1620                                sortable_key.clone(),
1621                                inclusive,
1622                                op.eq_only(),
1623                                direction,
1624                                self.tx_id,
1625                                &mut self.index_iterator,
1626                            )?;
1627
1628                            // Set MVCC peek
1629                            {
1630                                self.dual_peek.mvcc_peek = match &mvcc_rowid {
1631                                    Some(rid) => CursorPeek::Row {
1632                                        key: rid.row_id.clone(),
1633                                        versions: None,
1634                                    },
1635                                    None => CursorPeek::Exhausted,
1636                                };
1637                            }
1638                        }
1639                    }
1640
1641                    // Move to btree seek state
1642                    self.state.replace(MvccLazyCursorState::Seek(
1643                        SeekState::SeekBtree(SeekBtreeState::SeekBtree),
1644                        direction,
1645                    ));
1646                    inject_io_yield!(self, CursorYieldPoint::SeekStart);
1647                }
1648                Some(MvccLazyCursorState::Seek(SeekState::SeekBtree(_), direction)) => {
1649                    return_if_io!(self.seek_btree_and_set_peek(seek_key.clone(), op));
1650                    self.state
1651                        .replace(MvccLazyCursorState::Seek(SeekState::PickWinner, direction));
1652                    inject_io_yield!(self, CursorYieldPoint::SeekBtreeProgress);
1653                }
1654                Some(MvccLazyCursorState::Seek(SeekState::PickWinner, direction)) => {
1655                    // Pick winner and return result
1656                    // Now pick the winner based on direction
1657                    let winner = self.dual_peek.get_next(direction);
1658
1659                    // Clear seek state
1660                    self.state = None;
1661
1662                    if let Some((winner_key, in_btree, winner_versions)) = winner {
1663                        self.current_pos = CursorPosition::Loaded {
1664                            row_id: RowID {
1665                                table_id: self.table_id,
1666                                row_id: winner_key.clone(),
1667                            },
1668                            in_btree,
1669                            versions: winner_versions,
1670                        };
1671
1672                        if op.eq_only() {
1673                            // Check if the winner matches the seek key
1674                            let found = match &seek_key {
1675                                SeekKey::TableRowId(row_id) => winner_key == RowKey::Int(*row_id),
1676                                SeekKey::IndexKey(index_key) => {
1677                                    let RowKey::Record(found_key) = &winner_key else {
1678                                        panic!("Found rowid is not a record");
1679                                    };
1680                                    let MvccCursorType::Index(index_info) = &self.mv_cursor_type
1681                                    else {
1682                                        panic!("Index cursor expected");
1683                                    };
1684                                    let key_info: Vec<_> = index_info
1685                                        .key_info
1686                                        .iter()
1687                                        .take(index_key.column_count())
1688                                        .cloned()
1689                                        .collect();
1690                                    let cmp = compare_immutable(
1691                                        index_key.get_values()?,
1692                                        found_key.key.get_values()?,
1693                                        &key_info,
1694                                    );
1695                                    cmp.is_eq()
1696                                }
1697                            };
1698                            if found {
1699                                return Ok(IOResult::Done(SeekResult::Found));
1700                            } else {
1701                                return Ok(IOResult::Done(SeekResult::NotFound));
1702                            }
1703                        } else {
1704                            return Ok(IOResult::Done(SeekResult::Found));
1705                        }
1706                    } else {
1707                        // Nothing found in either cursor
1708                        let forwards = matches!(op, SeekOp::GE { .. } | SeekOp::GT);
1709                        if forwards {
1710                            self.current_pos = CursorPosition::End;
1711                        } else {
1712                            self.current_pos = CursorPosition::BeforeFirst;
1713                        }
1714                        return Ok(IOResult::Done(SeekResult::NotFound));
1715                    }
1716                }
1717                _ => {
1718                    panic!("Invalid state in seek: {:?}", self.state);
1719                }
1720            }
1721        }
1722    }
1723
1724    /// Insert a row into the table or index.
1725    /// Sets the cursor to the inserted row.
1726    fn insert(&mut self, key: &BTreeKey) -> Result<IOResult<()>> {
1727        let row_id = match key {
1728            BTreeKey::TableRowId((rowid, _)) => RowID::new(self.table_id, RowKey::Int(*rowid)),
1729            BTreeKey::IndexKey(record) => {
1730                let MvccCursorType::Index(index_info) = &self.mv_cursor_type else {
1731                    panic!("BTreeKey::IndexKey requires Index cursor type");
1732                };
1733                let sortable_key = Arc::new(SortableIndexKey::new_from_record(
1734                    (*record).clone(),
1735                    index_info.clone(),
1736                ));
1737                RowID::new(self.table_id, RowKey::Record(sortable_key))
1738            }
1739        };
1740        let row = match &self.mv_cursor_type {
1741            MvccCursorType::Table => {
1742                let BTreeKey::TableRowId((_, record)) = key else {
1743                    return Err(LimboError::InternalError(
1744                        "Table cursor requires a TableRowId key".to_string(),
1745                    ));
1746                };
1747                let record = record.as_ref().ok_or_else(|| {
1748                    LimboError::InternalError("TableRowId should have a record".to_string())
1749                })?;
1750                let num_columns = record.column_count();
1751                crate::with_mv_store_allocation_site!(
1752                    RowPayload,
1753                    Row::new_table_row_in(
1754                        row_id,
1755                        record.get_payload(),
1756                        num_columns,
1757                        self.db.allocator(),
1758                    )
1759                )
1760            }
1761            MvccCursorType::Index(_) => {
1762                let BTreeKey::IndexKey(record) = key else {
1763                    return Err(LimboError::InternalError(
1764                        "Index cursor requires an IndexKey".to_string(),
1765                    ));
1766                };
1767                Ok(Row::new_index_row(row_id, record.column_count()))
1768            }
1769        }?;
1770
1771        // Check if the cursor is currently positioned at a B-tree row that matches
1772        // the row we're inserting. This indicates we're updating a B-tree-resident row
1773        // that doesn't yet have an MVCC version.
1774        let was_btree_resident = match &self.current_pos {
1775            CursorPosition::Loaded {
1776                row_id: current_row_id,
1777                in_btree,
1778                ..
1779            } => *in_btree && *current_row_id == row.id,
1780            _ => false,
1781        };
1782
1783        self.current_pos = CursorPosition::Loaded {
1784            row_id: row.id.clone(),
1785            in_btree: was_btree_resident,
1786            versions: None,
1787        };
1788        let maybe_index_id = match &self.mv_cursor_type {
1789            MvccCursorType::Index(_) => Some(self.table_id),
1790            MvccCursorType::Table => None,
1791        };
1792        // FIXME: set btree to somewhere close to this rowid?
1793        if self
1794            .db
1795            .read_from_table_or_index(self.tx_id, &row.id, maybe_index_id)?
1796            .is_some()
1797        {
1798            self.db
1799                .update_to_table_or_index(self.tx_id, row, maybe_index_id)
1800                .inspect_err(|_| {
1801                    self.current_pos = CursorPosition::BeforeFirst;
1802                })?;
1803        } else if was_btree_resident {
1804            // The row exists in B-tree but not in MvStore - mark it as B-tree resident
1805            // so that checkpoint knows to write deletes to the B-tree file.
1806            self.db
1807                .insert_btree_resident_to_table_or_index(self.tx_id, row, maybe_index_id)
1808                .inspect_err(|_| {
1809                    self.current_pos = CursorPosition::BeforeFirst;
1810                })?;
1811        } else {
1812            self.db
1813                .insert_to_table_or_index(self.tx_id, row, maybe_index_id)
1814                .inspect_err(|_| {
1815                    self.current_pos = CursorPosition::BeforeFirst;
1816                })?;
1817        }
1818        self.invalidate_record();
1819        Ok(IOResult::Done(()))
1820    }
1821
1822    fn delete(&mut self) -> Result<IOResult<()>> {
1823        let (rowid, in_btree) = match self.get_current_pos() {
1824            CursorPosition::Loaded {
1825                row_id, in_btree, ..
1826            } => (row_id, in_btree),
1827            _ => panic!("Cannot delete: no current row"),
1828        };
1829        if in_btree {
1830            turso_assert!(
1831                self.is_btree_allocated(),
1832                "MVCC cursor marked current row as B-tree resident without an allocated B-tree",
1833                { "row_id": &rowid }
1834            );
1835        }
1836        let maybe_index_id = match &self.mv_cursor_type {
1837            MvccCursorType::Index(_) => Some(self.table_id),
1838            MvccCursorType::Table => None,
1839        };
1840        // If the cursor is positioned at a btree-resident row, the VDBE may never
1841        // have materialized the row's record (e.g. UPDATE through a DeferredSeek
1842        // never calls Column on the table cursor). Pre-fetch it here so the
1843        // later synchronous fetch used to build a tombstone doesn't have to
1844        // yield IO from inside this function, which is not IO-reentrant w.r.t.
1845        // `delete_from_table_or_index`'s side effects.
1846        if in_btree {
1847            return_if_io!(self.record());
1848        }
1849        let was_deleted =
1850            self.db
1851                .delete_from_table_or_index(self.tx_id, rowid.clone(), maybe_index_id)?;
1852        // If was_deleted is false, this can ONLY happen when we have a row that only exists
1853        // in the btree but not the mv store. In this case, we create a tombstone for the row
1854        // based on the btree row.
1855        if !was_deleted {
1856            // The cursor can also be positioned on a row that was rolled back
1857            // after seek. That row does not exist in either MVCC or the B-tree.
1858            if !in_btree {
1859                self.invalidate_record();
1860                return Ok(IOResult::Done(()));
1861            }
1862            // The btree cursor must be correctly positioned and cannot cause IO to happen
1863            // because we pre-fetched the record above when `in_btree` was true.
1864            let IOResult::Done(Some(record)) = self.record()? else {
1865                crate::bail_corrupt_error!(
1866                    "Btree cursor should have a record when deleting a row that only exists in the btree"
1867                );
1868            };
1869            // All operations below clone values so we can clone it here to circumvent the borrow checker
1870            let record = record.clone();
1871            let column_count = record.column_count();
1872            let row = match &self.mv_cursor_type {
1873                MvccCursorType::Table => crate::with_mv_store_allocation_site!(
1874                    RowPayload,
1875                    Row::new_table_row_in(
1876                        rowid.clone(),
1877                        record.get_payload(),
1878                        column_count,
1879                        self.db.allocator(),
1880                    )
1881                ),
1882                MvccCursorType::Index(_) => Ok(Row::new_index_row(rowid.clone(), column_count)),
1883            }?;
1884            self.db
1885                .insert_tombstone_to_table_or_index(self.tx_id, rowid, row, maybe_index_id)?;
1886        }
1887        self.invalidate_record();
1888        Ok(IOResult::Done(()))
1889    }
1890
1891    fn set_null_flag(&mut self, flag: bool) {
1892        self.null_flag = flag;
1893    }
1894
1895    fn get_null_flag(&self) -> bool {
1896        self.null_flag
1897    }
1898
1899    fn exists(&mut self, key: &Value) -> Result<IOResult<bool>> {
1900        if self.state.is_none() {
1901            self.invalidate_record();
1902            let int_key = match key {
1903                Value::Numeric(crate::numeric::Numeric::Integer(i)) => i,
1904                _ => unreachable!("btree tables are indexed by integers!"),
1905            };
1906            let inclusive = true;
1907
1908            // Check MVCC first. This is a point existence probe, so it is
1909            // eq-only: bound the skiplist walk to the single rowid instead of
1910            // scanning forward over invisible concurrent rows.
1911            let rowid = self.db.seek_rowid(
1912                RowID {
1913                    table_id: self.table_id,
1914                    row_id: RowKey::Int(*int_key),
1915                },
1916                inclusive,
1917                true,
1918                IterationDirection::Forwards,
1919                self.tx_id,
1920                &mut self.table_iterator,
1921            );
1922
1923            let mvcc_exists = if let Some(rowid) = &rowid {
1924                let RowKey::Int(rowid) = rowid.row_id else {
1925                    panic!("Rowid is not an integer in mvcc table cursor");
1926                };
1927                rowid == *int_key
1928            } else {
1929                false
1930            };
1931
1932            tracing::trace!(
1933                "MVCC exists check: mvcc_exists={mvcc_exists} find={int_key} got={rowid:?}"
1934            );
1935
1936            // If found in MVCC, update dual_peek and return true
1937            if mvcc_exists {
1938                self.dual_peek.mvcc_peek = CursorPeek::Row {
1939                    key: RowKey::Int(*int_key),
1940                    versions: None,
1941                };
1942                self.current_pos = CursorPosition::Loaded {
1943                    row_id: RowID {
1944                        table_id: self.table_id,
1945                        row_id: RowKey::Int(*int_key),
1946                    },
1947                    in_btree: false,
1948                    versions: None,
1949                };
1950                self.state = None;
1951                return Ok(IOResult::Done(true));
1952            }
1953
1954            // MVCC doesn't have it, but we need to check B-tree too
1955            if self.is_btree_allocated() {
1956                // Check if the B-tree version is valid (not shadowed/deleted by MVCC)
1957                let btree_is_valid = self.query_btree_version_is_valid(&RowKey::Int(*int_key));
1958
1959                // If B-tree is invalid (row is deleted or shadowed), don't check B-tree
1960                if !btree_is_valid {
1961                    self.state = None;
1962                    return Ok(IOResult::Done(false));
1963                }
1964                self.state
1965                    .replace(MvccLazyCursorState::Exists(ExistsState::ExistsBtree));
1966                inject_io_yield!(self, CursorYieldPoint::ExistsBtreeFallback);
1967            } else {
1968                // No B-tree allocated, row doesn't exist
1969                self.state = None;
1970                return Ok(IOResult::Done(false));
1971            }
1972        }
1973
1974        let Some(MvccLazyCursorState::Exists(ExistsState::ExistsBtree)) = self.state.clone() else {
1975            panic!("Invalid state {:?}", self.state);
1976        };
1977        turso_assert!(
1978            self.is_btree_allocated(),
1979            "BTree should be allocated when we are in ExistsBtree state"
1980        );
1981
1982        // Check if row exists in B-tree
1983        let found = return_if_io!(self.btree_cursor.exists(key));
1984
1985        if found {
1986            // Found in B-tree, but need to verify it's not shadowed by MVCC tombstone
1987            let int_key = match key {
1988                Value::Numeric(crate::numeric::Numeric::Integer(i)) => *i,
1989                _ => unreachable!("btree tables are indexed by integers!"),
1990            };
1991            let row_key = RowKey::Int(int_key);
1992
1993            // Check if this B-tree row is shadowed (deleted/updated) in MVCC
1994            let is_valid = self.query_btree_version_is_valid(&row_key);
1995
1996            if is_valid {
1997                // B-tree row is visible (not shadowed), update dual_peek
1998                self.dual_peek.btree_peek = CursorPeek::Row {
1999                    key: row_key.clone(),
2000                    versions: None,
2001                };
2002                self.current_pos = CursorPosition::Loaded {
2003                    row_id: RowID {
2004                        table_id: self.table_id,
2005                        row_id: row_key,
2006                    },
2007                    in_btree: true,
2008                    versions: None,
2009                };
2010                self.state = None;
2011                Ok(IOResult::Done(true))
2012            } else {
2013                // B-tree row is shadowed by MVCC (tombstone or update), so it doesn't exist
2014                tracing::trace!("B-tree row {int_key} is shadowed by MVCC");
2015                self.state = None;
2016                Ok(IOResult::Done(false))
2017            }
2018        } else {
2019            // Not found in B-tree either
2020            self.state = None;
2021            Ok(IOResult::Done(false))
2022        }
2023    }
2024
2025    fn clear_btree(&mut self) -> Result<IOResult<Option<usize>>> {
2026        todo!()
2027    }
2028
2029    fn btree_destroy(&mut self) -> Result<IOResult<Option<usize>>> {
2030        todo!()
2031    }
2032
2033    fn count(&mut self) -> Result<IOResult<usize>> {
2034        loop {
2035            let state = self.count_state;
2036            match state {
2037                None => {
2038                    self.count_state.replace(CountState::Rewind);
2039                    inject_io_yield!(self, CursorYieldPoint::CountProgress);
2040                }
2041                Some(CountState::Rewind) => {
2042                    return_if_io!(self.rewind());
2043                    self.count_state
2044                        .replace(CountState::CheckBtreeKey { count: 0 });
2045                    inject_io_yield!(self, CursorYieldPoint::CountProgress);
2046                }
2047                Some(CountState::CheckBtreeKey { count }) => {
2048                    if let CursorPosition::Loaded {
2049                        row_id: _,
2050                        in_btree: _,
2051                        ..
2052                    } = self.get_current_pos()
2053                    {
2054                        self.count_state
2055                            .replace(CountState::NextBtree { count: count + 1 });
2056                        inject_io_yield!(self, CursorYieldPoint::CountProgress);
2057                    } else {
2058                        self.count_state = None;
2059                        return Ok(IOResult::Done(count));
2060                    }
2061                }
2062                Some(CountState::NextBtree { count }) => {
2063                    // advance the btree cursor skips non valid keys
2064                    return_if_io!(self.next());
2065                    self.count_state
2066                        .replace(CountState::CheckBtreeKey { count });
2067                    inject_io_yield!(self, CursorYieldPoint::CountProgress);
2068                }
2069            }
2070        }
2071    }
2072
2073    /// Returns true if the is not pointing to any row.
2074    fn is_empty(&self) -> bool {
2075        // If we reached the end of the table, it means we traversed the whole table therefore there must be something in the table.
2076        // If we have loaded a row, it means there is something in the table.
2077        match self.get_current_pos() {
2078            CursorPosition::Loaded { .. } => false,
2079            CursorPosition::BeforeFirst => true,
2080            CursorPosition::End => true,
2081        }
2082    }
2083
2084    fn root_page(&self) -> i64 {
2085        self.table_id.into()
2086    }
2087
2088    fn rewind(&mut self) -> Result<IOResult<()>> {
2089        // A cursor may be NullRow'd during outer-join unmatched emission.
2090        // Repositioning to a real row must clear that synthetic NULL state.
2091        self.set_null_flag(false);
2092        let state = self.state.clone();
2093        if state.is_none() {
2094            let _ = self.table_iterator.take();
2095            let _ = self.index_iterator.take();
2096            self.reset_dual_peek();
2097            self.state
2098                .replace(MvccLazyCursorState::Rewind(RewindState::Advance));
2099        }
2100
2101        turso_assert!(
2102            matches!(
2103                self.state
2104                    .as_ref()
2105                    .expect("rewind state is not initialized"),
2106                MvccLazyCursorState::Rewind(RewindState::Advance)
2107            ),
2108            "invalid rewind state",
2109            { "state": format!("{:?}", self.state) }
2110        );
2111        // First run btree_cursor rewind so that we don't need a explicit state machine.
2112        return_if_io!(self.advance_btree_forward());
2113
2114        self.invalidate_record();
2115        self.current_pos = CursorPosition::BeforeFirst;
2116
2117        // Initialize MVCC iterators for rewind operation; in practice there is only one of these
2118        // depending on the cursor type, so we should at some point refactor the iterator thing to be
2119        // generic over the type instead of having two on the struct.
2120        match &self.mv_cursor_type {
2121            MvccCursorType::Table => {
2122                // For table cursors, initialize iterator from the correct table id + i64::MIN;
2123                // this is because table rows from all tables are stored in the same map
2124                let start_rowid = RowID {
2125                    table_id: self.table_id,
2126                    row_id: RowKey::Int(i64::MIN),
2127                };
2128                let range = (
2129                    std::ops::Bound::Included(start_rowid),
2130                    std::ops::Bound::Unbounded,
2131                );
2132                let iter_box = Box::new(self.db.rows.range(range));
2133                self.table_iterator = Some(static_iterator_hack!(iter_box, RowID, A));
2134            }
2135            MvccCursorType::Index(_) => {
2136                // For index cursors, initialize the iterator to the beginning
2137                let index_rows = self.db.get_or_create_index_rows(self.table_id)?;
2138                let index_rows = index_rows.value();
2139                let iter_box: Box<
2140                    dyn Iterator<Item = MvccEntry<'_, Arc<SortableIndexKey>, A>> + Send + Sync,
2141                > = Box::new(index_rows.iter());
2142                self.index_iterator =
2143                    Some(static_iterator_hack!(iter_box, Arc<SortableIndexKey>, A));
2144            }
2145        }
2146
2147        // Rewind mvcc iterator
2148        self.advance_mvcc_iterator();
2149
2150        self.refresh_current_position(IterationDirection::Forwards);
2151
2152        self.invalidate_record();
2153        self.state = None;
2154        Ok(IOResult::Done(()))
2155    }
2156
2157    fn has_record(&self) -> bool {
2158        matches!(self.get_current_pos(), CursorPosition::Loaded { .. })
2159    }
2160
2161    fn set_has_record(&mut self, _has_record: bool) {
2162        todo!()
2163    }
2164
2165    fn get_index_info(&self) -> &Arc<crate::types::IndexInfo> {
2166        match &self.mv_cursor_type {
2167            MvccCursorType::Index(index_info) => index_info,
2168            MvccCursorType::Table => panic!("get_index_info called on table cursor"),
2169        }
2170    }
2171
2172    fn seek_end(&mut self) -> Result<IOResult<()>> {
2173        if self.is_btree_allocated() {
2174            // Defer to btree cursor's seek_end implementation
2175            self.btree_cursor.seek_end()
2176        } else {
2177            // SkipMap inserts don't require cursor positioning because
2178            // SeekEnd instruction is only used for insertions.
2179            Ok(IOResult::Done(()))
2180        }
2181    }
2182
2183    fn seek_to_last(&mut self) -> Result<IOResult<()>> {
2184        match self.seek(SeekKey::TableRowId(i64::MAX), SeekOp::LE { eq_only: false })? {
2185            IOResult::Done(_) => Ok(IOResult::Done(())),
2186            IOResult::IO(iocompletions) => Ok(IOResult::IO(iocompletions)),
2187        }
2188    }
2189
2190    fn invalidate_record(&mut self) {
2191        if let Some(record) = self.reusable_immutable_record.as_mut() {
2192            record.invalidate();
2193        }
2194    }
2195
2196    fn has_rowid(&self) -> bool {
2197        match &self.mv_cursor_type {
2198            MvccCursorType::Index(index_info) => index_info.has_rowid,
2199            MvccCursorType::Table => true, // currently we don't support WITHOUT ROWID tables
2200        }
2201    }
2202
2203    fn get_pager(&self) -> Arc<Pager> {
2204        self.btree_cursor.get_pager()
2205    }
2206
2207    fn get_skip_advance(&self) -> bool {
2208        todo!()
2209    }
2210
2211    /// Returns true if this cursor operates in MVCC mode.
2212    fn is_mvcc(&self) -> bool {
2213        true
2214    }
2215}
2216
2217impl<Clock: LogicalClock, A: ConcurrentAllocator> Debug for MvccLazyCursor<Clock, A> {
2218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2219        f.debug_struct("MvccLazyCursor")
2220            .field("current_pos", &self.current_pos)
2221            .field("table_id", &self.table_id)
2222            .field("tx_id", &self.tx_id)
2223            .field("reusable_immutable_record", &self.reusable_immutable_record)
2224            .field("btree_cursor", &())
2225            .finish()
2226    }
2227}