Skip to main content

clt_database/vdbe/
mod.rs

1//! The virtual database engine (VDBE).
2//!
3//! The VDBE is a register-based virtual machine that execute bytecode
4//! instructions that represent SQL statements. When an application prepares
5//! an SQL statement, the statement is compiled into a sequence of bytecode
6//! instructions that perform the needed operations, such as reading or
7//! writing to a b-tree, sorting, or aggregating data.
8//!
9//! The instruction set of the VDBE is similar to SQLite's instruction set,
10//! but with the exception that bytecodes that perform I/O operations are
11//! return execution back to the caller instead of blocking. This is because
12//! Turso is designed for applications that need high concurrency such as
13//! serverless runtimes. In addition, asynchronous I/O makes storage
14//! disaggregation easier.
15//!
16//! You can find a full list of SQLite opcodes at:
17//!
18//! https://www.sqlite.org/opcode.html
19
20use crate::translate::plan::BitSet;
21use crate::types::{Extendable, Text};
22use crate::{turso_assert, turso_assert_ne, turso_debug_assert, NonNan};
23pub mod affinity;
24pub mod array;
25pub mod bloom_filter;
26pub mod builder;
27pub mod execute;
28pub mod explain;
29#[allow(dead_code)]
30pub mod hash_table;
31pub mod insn;
32pub mod metrics;
33pub mod rowset;
34pub mod sorter;
35#[cfg(clt_turso_tests)]
36mod statement_lifecycle_tests;
37pub mod vacuum;
38pub mod value;
39// for benchmarks
40pub use crate::translate::collate::CollationSeq;
41use crate::{
42    alloc::DynAllocator,
43    error::LimboError,
44    function::FuncCtx,
45    mvcc::{database::CommitStateMachine, MvccClock},
46    numeric::Numeric,
47    return_if_io,
48    schema::Trigger,
49    state_machine::StateMachine,
50    translate::plan::TableReferences,
51    types::{IOCompletions, IOResult},
52    vdbe::{
53        execute::{
54            OpAttachState, OpClearBtreeState, OpColumnState, OpDeleteState, OpDeleteSubState,
55            OpDestroyState, OpIdxInsertState, OpInitCdcVersionState, OpInsertState,
56            OpInsertSubState, OpJournalModeState, OpNewRowidState, OpNoConflictState,
57            OpParseSchemaState, OpProgramState, OpRowIdState, OpSeekState, OpTransactionState,
58            VacuumIntoOpContext,
59        },
60        hash_table::HashTable,
61        metrics::StatementMetrics,
62        vacuum::VacuumInPlaceOpContext,
63    },
64    ValueRef, WalAutoActions,
65};
66use smallvec::SmallVec;
67
68#[cfg(clt_turso_feature = "json")]
69use crate::json::JsonCacheCell;
70use crate::sync::RwLock;
71use crate::{
72    storage::pager::Pager,
73    translate::plan::ResultSetColumn,
74    types::{AggContext, Cursor, ImmutableRecord, Value},
75    vdbe::{builder::CursorType, insn::Insn},
76};
77use crate::{
78    AtomicBool, CaptureDataChangesInfo, Connection, MvStore, Result, Statement, SyncMode,
79    TransactionState,
80};
81use branches::{mark_unlikely, unlikely};
82use builder::{CursorKey, QueryMode};
83use execute::{
84    InsnFunction, InsnFunctionStepResult, OpIdxDeleteState, OpIntegrityCheckState,
85    OpOpenEphemeralState,
86};
87use turso_parser::ast::ResolveType;
88
89use crate::io::TempFile;
90use crate::vdbe::bloom_filter::BloomFilter;
91use crate::vdbe::rowset::RowSet;
92use explain::{insn_to_row_with_comment, EXPLAIN_COLUMNS, EXPLAIN_QUERY_PLAN_COLUMNS};
93use std::{
94    collections::HashMap,
95    num::NonZero,
96    ops::Deref,
97    sync::{
98        atomic::{AtomicI64, AtomicIsize, Ordering},
99        Arc,
100    },
101    task::Waker,
102};
103use tracing::{instrument, Level};
104
105type MvccCommitStateMachine = CommitStateMachine<MvccClock, DynAllocator>;
106
107/// State machine for committing view deltas with I/O handling
108#[derive(Debug, Clone)]
109pub enum ViewDeltaCommitState {
110    NotStarted,
111    Processing {
112        views: Vec<String>, // view names (all materialized views have storage)
113        current_index: usize,
114    },
115    Done,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119/// Represents a target for a jump instruction.
120/// Stores 32-bit ints to keep the enum word-sized.
121pub enum BranchOffset {
122    /// A label is a named location in the program.
123    /// If there are references to it, it must always be resolved to an Offset
124    /// via `ProgramBuilder::preassign_label_to_next_insn` or
125    /// `ProgramBuilder::link_label_to_other_label`.
126    Label(u32),
127    /// An offset is a direct index into the instruction list.
128    Offset(InsnReference),
129    /// A placeholder is a temporary value to satisfy the compiler.
130    /// It must be set later.
131    Placeholder,
132}
133
134impl BranchOffset {
135    /// Returns true if the branch offset is an offset.
136    pub fn is_offset(&self) -> bool {
137        matches!(self, BranchOffset::Offset(_))
138    }
139
140    /// Returns the offset value. Panics if the branch offset is a label or placeholder.
141    pub fn as_offset_int(&self) -> InsnReference {
142        match self {
143            BranchOffset::Label(v) => unreachable!("Unresolved label: {}", v),
144            BranchOffset::Offset(v) => *v,
145            BranchOffset::Placeholder => unreachable!("Unresolved placeholder"),
146        }
147    }
148
149    /// Returns the branch offset as a signed integer.
150    /// Used in explain output, where we don't want to panic in case we have an unresolved
151    /// label or placeholder.
152    pub fn as_debug_int(&self) -> i32 {
153        match self {
154            BranchOffset::Label(v) => *v as i32,
155            BranchOffset::Offset(v) => *v as i32,
156            BranchOffset::Placeholder => i32::MAX,
157        }
158    }
159}
160
161pub type CursorID = usize;
162
163pub type PageIdx = i64;
164
165// Index of insn in list of insns
166type InsnReference = u32;
167
168#[derive(Debug)]
169pub enum StepResult {
170    Done,
171    IO,
172    Row,
173    Interrupt,
174    Busy,
175    /// The statement explicitly yielded control back to the caller without any pending I/O.
176    /// Stepping again immediately (even in a tight loop) is fine; blocking callers should
177    /// still drive the event loop (`io.step()`) between steps so progress that depends on
178    /// other threads' I/O is not starved.
179    Yield,
180}
181
182#[derive(Debug)]
183#[allow(clippy::large_enum_variant)]
184/// The commit state of the program.
185/// There are two states:
186/// - Ready: The program is ready to run the next instruction, or has shut down after
187///   the last instruction.
188/// - Committing: The program is committing a write transaction. It is waiting for the pager to finish flushing the cache to disk,
189///   primarily to the WAL, but also possibly checkpointing the WAL to the database file.
190enum CommitState {
191    Ready,
192    Committing,
193    /// Committing attached database pagers after main pager commit is done.
194    CommittingAttached,
195    CommittingMvcc {
196        state_machine: StateMachine<Box<MvccCommitStateMachine>>,
197    },
198    /// Committing MVCC transactions on attached databases after main MVCC commit is done.
199    CommittingAttachedMvcc {
200        state_machine: StateMachine<Box<MvccCommitStateMachine>>,
201        db_id: usize,
202        mv_store: Arc<MvStore>,
203    },
204}
205
206impl CommitState {
207    fn cleanup_mvcc_checkpoint_state(&mut self) {
208        match self {
209            CommitState::CommittingMvcc { state_machine } => {
210                state_machine.inner_mut().cleanup_mvcc_checkpoint_state()
211            }
212            CommitState::CommittingAttachedMvcc { state_machine, .. } => {
213                state_machine.inner_mut().cleanup_mvcc_checkpoint_state()
214            }
215            CommitState::Ready | CommitState::Committing | CommitState::CommittingAttached => {}
216        }
217    }
218
219    fn cleanup_abandoned_mvcc_commit(&mut self, connection: &Connection) {
220        match self {
221            CommitState::CommittingAttachedMvcc {
222                state_machine,
223                db_id: attached_db_id,
224                ..
225            } if !state_machine.is_finalized() => {
226                if connection
227                    .database_schemas()
228                    .write()
229                    .remove(attached_db_id)
230                    .is_some()
231                {
232                    connection.bump_prepare_context_generation();
233                }
234            }
235            CommitState::CommittingMvcc { state_machine } if !state_machine.is_finalized() => {}
236            _ => return, // no-op for already-finalized state machines and non-MVCC commit states
237        };
238
239        // Replace the live CommitState with Ready so the abandoned state machine
240        // drops here. CommitStateMachine::Drop -> cleanup_unfinished_commit ->
241        // cleanup_dropped_commit ultimately calls rollback_tx_inner on a tx left
242        // in Active/Preparing. Without this, the orphan tx stays Preparing
243        // forever — any other transaction that took a commit dependency on it
244        // (Hekaton §2.7 speculative read) deadlocks in WaitForDependencies.
245        // The locks/exclusive slot the SM acquired are released by the same
246        // cleanup path on drop.
247        *self = CommitState::Ready;
248
249        connection.rollback_attached_mvcc_txs(true);
250        connection.rollback_attached_wal_txns();
251        connection.rollback_temp_schema();
252    }
253}
254
255#[derive(Debug, Clone, PartialEq)]
256pub enum Register {
257    Value(Value),
258    Aggregate(AggContext),
259    Record(ImmutableRecord),
260}
261
262impl Register {
263    #[inline]
264    pub const fn is_null(&self) -> bool {
265        matches!(self, Register::Value(Value::Null))
266    }
267
268    #[inline(always)]
269    /// Sets the value of the register to an integer,
270    /// reusing the existing Register::Value(Value::Numeric(Numeric::Integer(_))) if possible,
271    /// which is faster than always creating a new one.
272    pub fn set_int(&mut self, val: i64) {
273        match self {
274            Register::Value(Value::Numeric(Numeric::Integer(existing))) => {
275                *existing = val;
276            }
277            Register::Value(Value::Numeric(float)) => {
278                *float = Numeric::Integer(val);
279            }
280            Register::Value(other_value_kind) => {
281                *other_value_kind = Value::from_i64(val);
282            }
283            _ => {
284                *self = Register::Value(Value::from_i64(val));
285            }
286        }
287    }
288    /// Set the value of the register to a floating point,
289    /// reusing Register::Value(Value::Numeric(Numeric::Float(_))) if possible.
290    #[inline(always)]
291    pub fn set_float(&mut self, val: NonNan) {
292        match self {
293            Register::Value(Value::Numeric(Numeric::Float(existing))) => {
294                *existing = val;
295            }
296            Register::Value(Value::Numeric(integer)) => {
297                *integer = Numeric::Float(val);
298            }
299            Register::Value(other_value_kind) => {
300                *other_value_kind = Value::Numeric(Numeric::Float(val));
301            }
302            _ => {
303                *self = Register::Value(Value::Numeric(Numeric::Float(val)));
304            }
305        }
306    }
307
308    /// Set the value of the register to a Text,
309    /// reusing Register::Value(Value::Text(_)) buffer if possible.
310    #[inline]
311    pub fn set_text(&mut self, val: Text) -> Result<()> {
312        match self {
313            Register::Value(Value::Text(existing)) => {
314                existing.do_extend(&val)?;
315            }
316            Register::Value(other_value_kind) => {
317                *other_value_kind = Value::Text(val);
318            }
319            _ => {
320                *self = Register::Value(Value::Text(val));
321            }
322        }
323        Ok(())
324    }
325
326    /// Set the value of the register to a blob,
327    /// reusing Register::Value(Value::Blob(_)) buffer if possible.
328    #[inline]
329    pub fn set_blob(&mut self, val: Vec<u8>) -> Result<()> {
330        match self {
331            Register::Value(Value::Blob(existing)) => {
332                existing.do_extend(&val)?;
333            }
334            Register::Value(other_value_kind) => {
335                *other_value_kind = Value::Blob(val);
336            }
337            _ => {
338                *self = Register::Value(Value::Blob(val));
339            }
340        }
341        Ok(())
342    }
343
344    // Set the value of the register to NULL,
345    // reusing the existing Register::Value(Value::Null) if possible.
346    pub fn set_null(&mut self) {
347        match self {
348            Register::Value(Value::Null) => {}
349            Register::Value(other_value_kind) => {
350                *other_value_kind = Value::Null;
351            }
352            _ => {
353                *self = Register::Value(Value::Null);
354            }
355        }
356    }
357
358    /// Set the register to a generic Value, attempting to reuse backing allocation if compatible.
359    pub fn set_value(&mut self, val: Value) {
360        match self {
361            Register::Value(v) => {
362                *v = val;
363            }
364            _ => {
365                *self = Register::Value(val);
366            }
367        }
368    }
369}
370
371/// A row is a the list of registers that hold the values for a filtered row. This row is a pointer, therefore
372/// after stepping again, row will be invalidated to be sure it doesn't point to somewhere unexpected.
373#[derive(Debug)]
374pub struct Row {
375    values: *const Register,
376    count: usize,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq)]
380pub enum TxnCleanup {
381    None,
382    RollbackTxn,
383    /// begin_statement was called and statement is participating in an interactive transaction.
384    /// If statement is abandoned and/or dropped without an apparent error, we should rollback statement
385    /// to previous savepoint.
386    RollbackSavepoint,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq)]
390pub enum ProgramExecutionState {
391    /// No steps of the program was executed
392    Init,
393    /// Program started execution but didn't reach any terminal state
394    Running,
395    /// Interrupt requested for the program
396    Interrupting,
397    /// Terminal state: program interrupted
398    Interrupted,
399    /// Terminal state: program finished successfully
400    Done,
401    /// Terminal state: program failed with error
402    Failed,
403}
404
405impl ProgramExecutionState {
406    pub const fn is_running(&self) -> bool {
407        matches!(
408            self,
409            ProgramExecutionState::Interrupting | ProgramExecutionState::Running
410        )
411    }
412    pub const fn is_terminal(&self) -> bool {
413        matches!(
414            self,
415            ProgramExecutionState::Interrupted
416                | ProgramExecutionState::Failed
417                | ProgramExecutionState::Done
418        )
419    }
420}
421
422/// Re-entrant state for [Insn::HashBuild].
423/// Allows HashBuild to resume cleanly after async I/O without re-reading the row.
424#[derive(Debug)]
425pub struct OpHashBuildState {
426    pub key_values: crate::alloc::Vec<Value>,
427    pub key_idx: usize,
428    pub payload_values: crate::alloc::Vec<Value>,
429    pub payload_idx: usize,
430    pub rowid: Option<i64>,
431    pub cursor_id: CursorID,
432    pub hash_table_id: usize,
433    pub key_start_reg: usize,
434    pub num_keys: usize,
435}
436
437/// Re-entrant state for [Insn::HashProbe].
438/// Allows HashProbe to resume cleanly after async probe-row buffering I/O.
439#[derive(Debug)]
440pub struct OpHashProbeState {
441    /// Cached probe key values to avoid re-reading from registers
442    pub probe_keys: crate::alloc::Vec<Value>,
443    /// Hash table register being probed
444    pub hash_table_id: usize,
445    /// Partition index being loaded (if any)
446    pub partition_idx: usize,
447    /// Whether the probe row was already buffered for grace processing.
448    pub probe_buffered: bool,
449}
450
451enum ActiveOpState {
452    None,
453    ClearBtree(OpClearBtreeState),
454    Delete(OpDeleteState),
455    Destroy(OpDestroyState),
456    IdxDelete(OpIdxDeleteState),
457    IntegrityCheck(OpIntegrityCheckState),
458    OpenEphemeral(OpOpenEphemeralState),
459    Program(OpProgramState),
460    NewRowid(OpNewRowidState),
461    IdxInsert(OpIdxInsertState),
462    Insert(OpInsertState),
463    NoConflict(OpNoConflictState),
464    Column(OpColumnState),
465    RowId(OpRowIdState),
466    Transaction(OpTransactionState),
467    Attach(OpAttachState),
468    JournalMode(OpJournalModeState),
469    ParseSchema(OpParseSchemaState),
470    HashBuild(Option<OpHashBuildState>),
471    HashProbe(Option<OpHashProbeState>),
472    InitCdcVersion(OpInitCdcVersionState),
473}
474
475impl std::fmt::Debug for ActiveOpState {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        let name = match self {
478            ActiveOpState::None => "None",
479            ActiveOpState::ClearBtree(_) => "ClearBtree",
480            ActiveOpState::Delete(_) => "Delete",
481            ActiveOpState::Destroy(_) => "Destroy",
482            ActiveOpState::IdxDelete(_) => "IdxDelete",
483            ActiveOpState::IntegrityCheck(_) => "IntegrityCheck",
484            ActiveOpState::OpenEphemeral(_) => "OpenEphemeral",
485            ActiveOpState::Program(_) => "Program",
486            ActiveOpState::NewRowid(_) => "NewRowid",
487            ActiveOpState::IdxInsert(_) => "IdxInsert",
488            ActiveOpState::Insert(_) => "Insert",
489            ActiveOpState::NoConflict(_) => "NoConflict",
490            ActiveOpState::Column(_) => "Column",
491            ActiveOpState::RowId(_) => "RowId",
492            ActiveOpState::Transaction(_) => "Transaction",
493            ActiveOpState::Attach(_) => "Attach",
494            ActiveOpState::JournalMode(_) => "JournalMode",
495            ActiveOpState::ParseSchema(_) => "ParseSchema",
496            ActiveOpState::HashBuild(_) => "HashBuild",
497            ActiveOpState::HashProbe(_) => "HashProbe",
498            ActiveOpState::InitCdcVersion(_) => "InitCdcVersion",
499        };
500        f.write_str(name)
501    }
502}
503
504#[derive(Debug, Default)]
505struct ActiveOpStateSlot {
506    state: ActiveOpState,
507}
508
509macro_rules! active_state_accessor {
510    ($name:ident, $variant:ident, $ty:ty, $init:expr) => {
511        fn $name(&mut self) -> &mut $ty {
512            if matches!(self.state, ActiveOpState::None) {
513                self.state = ActiveOpState::$variant($init);
514            }
515            match &mut self.state {
516                ActiveOpState::$variant(state) => state,
517                state => unreachable!(
518                    "active opcode state mismatch: expected {}, got {:?}",
519                    stringify!($variant),
520                    state
521                ),
522            }
523        }
524    };
525}
526
527impl Default for ActiveOpState {
528    fn default() -> Self {
529        Self::None
530    }
531}
532
533impl ActiveOpStateSlot {
534    fn clear(&mut self) {
535        self.state = ActiveOpState::None;
536    }
537
538    active_state_accessor!(
539        delete,
540        Delete,
541        OpDeleteState,
542        OpDeleteState {
543            sub_state: OpDeleteSubState::MaybeCaptureRecord,
544            deleted_record: None,
545        }
546    );
547    active_state_accessor!(
548        clear_btree,
549        ClearBtree,
550        OpClearBtreeState,
551        OpClearBtreeState::CreateCursor
552    );
553    active_state_accessor!(
554        destroy,
555        Destroy,
556        OpDestroyState,
557        OpDestroyState::CreateCursor
558    );
559    active_state_accessor!(
560        idx_delete,
561        IdxDelete,
562        OpIdxDeleteState,
563        OpIdxDeleteState::Seeking
564    );
565    active_state_accessor!(
566        integrity_check,
567        IntegrityCheck,
568        OpIntegrityCheckState,
569        OpIntegrityCheckState::Start
570    );
571    active_state_accessor!(
572        open_ephemeral,
573        OpenEphemeral,
574        OpOpenEphemeralState,
575        OpOpenEphemeralState::Start
576    );
577    active_state_accessor!(program, Program, OpProgramState, OpProgramState::Start);
578    active_state_accessor!(new_rowid, NewRowid, OpNewRowidState, OpNewRowidState::Start);
579    active_state_accessor!(
580        idx_insert,
581        IdxInsert,
582        OpIdxInsertState,
583        OpIdxInsertState::MaybeSeek
584    );
585    active_state_accessor!(
586        insert,
587        Insert,
588        OpInsertState,
589        OpInsertState {
590            sub_state: OpInsertSubState::MaybeCaptureRecord,
591            old_record: None,
592            is_noop_update: false,
593        }
594    );
595    active_state_accessor!(
596        no_conflict,
597        NoConflict,
598        OpNoConflictState,
599        OpNoConflictState::Start
600    );
601    active_state_accessor!(column, Column, OpColumnState, OpColumnState::Start);
602    active_state_accessor!(row_id, RowId, OpRowIdState, OpRowIdState::Start);
603    active_state_accessor!(
604        transaction,
605        Transaction,
606        OpTransactionState,
607        OpTransactionState::Start
608    );
609    active_state_accessor!(attach, Attach, OpAttachState, OpAttachState::default());
610    active_state_accessor!(
611        journal_mode,
612        JournalMode,
613        OpJournalModeState,
614        OpJournalModeState::default()
615    );
616    active_state_accessor!(parse_schema, ParseSchema, OpParseSchemaState, None);
617    active_state_accessor!(hash_build, HashBuild, Option<OpHashBuildState>, None);
618    active_state_accessor!(hash_probe, HashProbe, Option<OpHashProbeState>, None);
619    active_state_accessor!(
620        init_cdc_version,
621        InitCdcVersion,
622        OpInitCdcVersionState,
623        None
624    );
625
626    /// Take the ParseSchema op state if it is the active one, without
627    /// touching (or panicking on) any other live op state. Used by abort
628    /// cleanup, which runs regardless of which opcode was executing.
629    fn take_parse_schema_if_active(&mut self) -> execute::OpParseSchemaState {
630        if let ActiveOpState::ParseSchema(inner) = &mut self.state {
631            let taken = inner.take();
632            self.state = ActiveOpState::None;
633            taken
634        } else {
635            None
636        }
637    }
638
639    fn program_ref(&self) -> Option<&OpProgramState> {
640        match &self.state {
641            ActiveOpState::Program(state) => Some(state),
642            _ => None,
643        }
644    }
645
646    fn program_mut(&mut self) -> Option<&mut OpProgramState> {
647        match &mut self.state {
648            ActiveOpState::Program(state) => Some(state),
649            _ => None,
650        }
651    }
652}
653
654#[derive(Debug, Clone)]
655pub(crate) struct DeferredSeekState {
656    pub index_cursor_id: CursorID,
657    pub table_cursor_id: CursorID,
658}
659
660pub(crate) enum VacuumOpState {
661    None,
662    IntoFile(Box<VacuumIntoOpContext>),
663    InPlace(Box<VacuumInPlaceOpContext>),
664}
665
666impl Default for VacuumOpState {
667    fn default() -> Self {
668        Self::None
669    }
670}
671
672/// The program state describes the environment in which the program executes.
673/// Bookkeeping for an in-flight sequence inner-tx wrap. The
674/// `SequenceBeginInnerTx` opcode populates this on the Wrapped path
675/// so a subsequent reset / unwind can roll back the inner tx and
676/// restore `conn.mv_tx_for_db(db)` even when the statement aborts
677/// before reaching `SequenceCommitInnerTx`.
678#[derive(Clone)]
679pub struct SequenceInnerTxState {
680    pub db: usize,
681    pub inner_tx_id: crate::mvcc::database::TxID,
682    pub saved_outer: Option<(
683        crate::mvcc::database::TxID,
684        crate::translate::emitter::TransactionMode,
685    )>,
686}
687
688pub struct ProgramState {
689    pub io_completions: Option<IOCompletions>,
690    pub pc: InsnReference,
691    pub(crate) cursors: Vec<Option<Cursor>>,
692    cursor_seqs: Vec<i64>,
693    registers: Box<[Register]>,
694    /// Trace state: register snapshot for diffing.
695    pre_op_registers: Option<Box<[Register]>>,
696    pub(crate) result_row: Option<Row>,
697    last_compare: Option<std::cmp::Ordering>,
698    deferred_seeks: Vec<Option<DeferredSeekState>>,
699    /// Indicate whether a coroutine has ended for a given yield register.
700    /// If an element is present, it means the coroutine with the given register number has ended.
701    ended_coroutine: Vec<u32>,
702    /// Indicate whether an [Insn::Once] instruction at a given program counter position has already been executed, well, once.
703    once: SmallVec<[u32; 4]>,
704    pub execution_state: ProgramExecutionState,
705    /// Per-execution statement deadline derived from the connection query timeout.
706    /// `None` means no timeout.
707    pub query_deadline: Option<crate::MonotonicInstant>,
708    pub parameters: Vec<Value>,
709    commit_state: CommitState,
710    /// In-flight commit-state-machine for an autonomous sequence
711    /// inner-tx. `Insn::SequenceCommitInnerTx` constructs this on first
712    /// entry and drives it one step per opcode call, yielding
713    /// `InsnFunctionStepResult::IO` between steps. Cleared on terminal
714    /// outcome (Done / Conflict / Err) and on statement reset.
715    pub sequence_inner_commit: Option<StateMachine<Box<MvccCommitStateMachine>>>,
716    /// State for a pending sequence inner-tx wrap, set by
717    /// `Insn::SequenceBeginInnerTx` (Wrapped path only) and cleared
718    /// by `Insn::SequenceCommitInnerTx` on any terminal outcome.
719    /// Tracked here (rather than in registers) so that statement
720    /// reset can roll back an orphaned inner tx and restore the
721    /// connection's mv_tx — registers are wiped on reset, but the
722    /// orphaned inner would otherwise linger in `mv_store.txs` and
723    /// pollute the connection's mv_tx slot, breaking subsequent
724    /// commits with phantom dependencies.
725    pub sequence_inner_tx_pending: Option<SequenceInnerTxState>,
726    /// Consecutive conflict-retry count for the in-progress sequence
727    /// inner-tx wrap. Incremented on each `WriteWriteConflict` /
728    /// `BusySnapshot` from `SequenceCommitInnerTx`; reset on the next
729    /// successful commit. When it exceeds
730    /// `SEQUENCE_INNER_TX_RETRY_BUDGET` the opcode returns
731    /// `LimboError::Busy` directly instead of routing a phony
732    /// `SQLITE_BUSY` halt through `op_halt`, which would mis-wrap it as
733    /// a constraint error.
734    pub sequence_inner_retry_count: u32,
735    #[cfg(clt_turso_feature = "json")]
736    json_cache: JsonCacheCell,
737    active_op_state: ActiveOpStateSlot,
738    seek_state: OpSeekState,
739    /// Metrics collected for the lifetime of this prepared statement.
740    pub metrics: StatementMetrics,
741    /// Current collation sequence set by OP_CollSeq instruction
742    current_collation: Option<CollationSeq>,
743    op_vacuum_state: VacuumOpState,
744    /// State machine for committing view deltas with I/O handling
745    view_delta_state: ViewDeltaCommitState,
746    /// Marker which tells about auto transaction cleanup necessary for that connection in case of reset
747    /// This is used when statement in auto-commit mode reseted after previous uncomplete execution - in which case we may need to rollback transaction started on previous attempt
748    pub(crate) auto_txn_cleanup: TxnCleanup,
749    pub explain_state: RwLock<ExplainState>,
750    /// Scratch buffer for [Insn::HashDistinct] to avoid per-row allocations.
751    distinct_key_values: Vec<Value>,
752    hash_tables: HashMap<usize, HashTable>,
753    /// TempFile handles for ephemeral cursors, keyed by cursor_id.
754    /// Dropping removes the temp file from disk.
755    ephemeral_temp_files: HashMap<usize, TempFile>,
756    /// Attached pagers that have open savepoints for statement rollback.
757    attached_savepoint_pagers: Vec<Arc<Pager>>,
758    /// Pending error to return after FAIL mode commit completes.
759    /// When a constraint error occurs with FAIL resolve type in autocommit mode,
760    /// we need to commit partial changes before returning the error.
761    pub(crate) pending_fail_error: Option<LimboError>,
762    /// Pending CDC info to apply after the program completes successfully.
763    /// Set by InitCdcVersion opcode, applied at Halt/Done so that if the
764    /// transaction rolls back, the connection's CDC state remains unchanged.
765    ///
766    /// capture_data_changes has type Option<CaptureDataChangesInfo> (off mode is None)
767    /// so, for pending_cdc_info we wrap it in one more Option<...> layer to represent if mode changed during program execution
768    pub(crate) pending_cdc_info: Option<Option<CaptureDataChangesInfo>>,
769    /// Cached subprogram Statements keyed by the PC of the Program instruction.
770    /// Avoids re-allocating ProgramState on each trigger/FK-action fire.
771    pub(crate) subprogram_stmt_cache: HashMap<usize, Box<Statement>>,
772    /// RowSet objects stored by register index
773    rowsets: HashMap<usize, RowSet>,
774    /// Bloom filters stored by cursor ID for probabilistic set membership testing
775    /// Used to avoid unnecessary seeks on ephemeral indexes and hash tables
776    pub(crate) bloom_filters: HashMap<usize, BloomFilter>,
777    /// Number of deferred foreign key violations when the statement started.
778    /// When a statement subtransaction rolls back, the connection's deferred foreign key violations counter
779    /// is reset to this value.
780    fk_deferred_violations_when_stmt_started: AtomicIsize,
781    /// Number of immediate foreign key violations that occurred during the active statement. If nonzero,
782    /// the statement subtransactionwill roll back.
783    fk_immediate_violations_during_stmt: AtomicIsize,
784    uses_subjournal: bool,
785    /// Whether this statement is an active write inside an explicit transaction.
786    pub(crate) is_active_write: bool,
787    /// Whether begin_statement was called (savepoint + FK bookkeeping active).
788    has_stmt_transaction: bool,
789    pub n_change: AtomicI64,
790    pub n_total_change: AtomicI64,
791}
792
793impl std::fmt::Debug for Program {
794    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
795        f.debug_struct("Program").finish()
796    }
797}
798
799// See: https://github.com/tursodatabase/turso/issues/1552
800// SAFETY: Rust cannot derive Send + Sync automatically mainly because of `Row` struct
801// as it contains a `*const Register`.
802// Program + Program State upholds Rust aliasing rules with `Row` by only giving out immutable references to
803// the internal `result_row` and by invalidating the result row whenever the program is stepped.
804unsafe impl Send for ProgramState {}
805unsafe impl Sync for ProgramState {}
806crate::assert::assert_send_sync!(ProgramState);
807
808impl ProgramState {
809    pub fn new(max_registers: usize, max_cursors: usize) -> Self {
810        let cursors: Vec<Option<Cursor>> = (0..max_cursors).map(|_| None).collect();
811        let cursor_seqs = vec![0i64; max_cursors];
812        let registers = vec![Register::Value(Value::Null); max_registers].into_boxed_slice();
813        Self {
814            io_completions: None,
815            pc: 0,
816            cursors,
817            cursor_seqs,
818            registers,
819            pre_op_registers: None,
820            result_row: None,
821            last_compare: None,
822            deferred_seeks: vec![None; max_cursors],
823            ended_coroutine: vec![],
824            once: SmallVec::<[u32; 4]>::new(),
825            execution_state: ProgramExecutionState::Init,
826            query_deadline: None,
827            parameters: Vec::new(),
828            commit_state: CommitState::Ready,
829            sequence_inner_commit: None,
830            sequence_inner_tx_pending: None,
831            sequence_inner_retry_count: 0,
832            #[cfg(clt_turso_feature = "json")]
833            json_cache: JsonCacheCell::new(),
834            active_op_state: ActiveOpStateSlot::default(),
835            seek_state: OpSeekState::Start,
836            metrics: StatementMetrics::new(),
837            distinct_key_values: Vec::new(),
838            current_collation: None,
839            op_vacuum_state: VacuumOpState::None,
840            view_delta_state: ViewDeltaCommitState::NotStarted,
841            auto_txn_cleanup: TxnCleanup::None,
842            fk_deferred_violations_when_stmt_started: AtomicIsize::new(0),
843            fk_immediate_violations_during_stmt: AtomicIsize::new(0),
844            rowsets: HashMap::default(),
845            bloom_filters: HashMap::default(),
846            hash_tables: HashMap::default(),
847            ephemeral_temp_files: HashMap::default(),
848            uses_subjournal: false,
849            is_active_write: false,
850            has_stmt_transaction: false,
851            attached_savepoint_pagers: Vec::new(),
852            n_change: AtomicI64::new(0),
853            n_total_change: AtomicI64::new(0),
854            explain_state: RwLock::new(ExplainState::default()),
855            pending_fail_error: None,
856            pending_cdc_info: None,
857            subprogram_stmt_cache: HashMap::default(),
858        }
859    }
860
861    pub fn set_register(&mut self, idx: usize, value: Register) {
862        self.registers[idx] = value;
863    }
864
865    pub fn get_register(&self, idx: usize) -> &Register {
866        &self.registers[idx]
867    }
868
869    pub fn column_count(&self) -> usize {
870        self.registers.len()
871    }
872
873    pub fn column(&self, i: usize) -> Option<String> {
874        Some(format!("{:?}", self.registers[i]))
875    }
876
877    pub fn interrupt(&mut self) {
878        self.execution_state = ProgramExecutionState::Interrupting;
879    }
880
881    pub fn is_interrupted(&self) -> bool {
882        matches!(self.execution_state, ProgramExecutionState::Interrupting)
883    }
884
885    pub fn bind_at(&mut self, index: NonZero<usize>, value: Value) -> Result<()> {
886        let i = index.get() - 1;
887        if i >= self.parameters.len() {
888            self.parameters.resize(i + 1, Value::Null);
889        }
890        let slot = &mut self.parameters[i];
891        match (slot, value) {
892            (Value::Null, Value::Null) => {}
893            (Value::Numeric(Numeric::Integer(existing)), Value::Numeric(Numeric::Integer(new))) => {
894                *existing = new
895            }
896            (Value::Numeric(Numeric::Float(existing)), Value::Numeric(Numeric::Float(new))) => {
897                *existing = new
898            }
899            (Value::Text(existing), Value::Text(new)) => existing.do_extend(&new)?,
900            (Value::Blob(existing), Value::Blob(new)) => existing.do_extend(&new)?,
901            (slot, value) => *slot = value,
902        }
903        Ok(())
904    }
905
906    pub fn clear_bindings(&mut self) {
907        self.parameters.clear();
908    }
909
910    pub fn get_parameter(&self, index: NonZero<usize>) -> Value {
911        let i = index.get() - 1;
912        self.parameters.get(i).cloned().unwrap_or(Value::Null)
913    }
914
915    pub fn reset(&mut self, max_registers: Option<usize>, max_cursors: Option<usize>) {
916        self.io_completions = None;
917        self.pc = 0;
918
919        if let Some(max_cursors) = max_cursors {
920            self.cursors.resize_with(max_cursors, || None);
921            self.cursor_seqs.resize(max_cursors, 0);
922            self.deferred_seeks.resize(max_cursors, None);
923        }
924        self.result_row = None;
925        if let Some(max_registers) = max_registers {
926            // into_vec and into_boxed_slice do not allocate
927            let mut registers = std::mem::take(&mut self.registers).into_vec();
928            // As we are dropping whatever is in the result row, we can be sure that no one is referencing values from `*const Register` inside `Row`.
929            registers.resize_with(max_registers, || Register::Value(Value::Null));
930            self.registers = registers.into_boxed_slice();
931        }
932        // reset cursors as they can have cached information which will be no longer relevant on next program execution
933        self.cursors.iter_mut().for_each(|c| {
934            let _ = c.take();
935        });
936        for r in self.registers.iter_mut() {
937            match r {
938                Register::Value(v) => *v = Value::Null,
939                _ => r.set_null(),
940            }
941        }
942        self.last_compare = None;
943        self.deferred_seeks.iter_mut().for_each(|s| *s = None);
944        self.ended_coroutine.clear();
945        self.once.clear();
946        self.execution_state = ProgramExecutionState::Init;
947        self.query_deadline = None;
948        self.current_collation = None;
949        #[cfg(clt_turso_feature = "json")]
950        self.json_cache.clear();
951
952        // A caller can reset or drop a statement after an MVCC auto-checkpoint
953        // has yielded I/O. Waiting for that I/O does not step the nested
954        // CheckpointStateMachine again, so release its checkpoint lock before
955        // replacing commit_state with Ready.
956        self.commit_state.cleanup_mvcc_checkpoint_state();
957        self.active_op_state.clear();
958        self.seek_state = OpSeekState::Start;
959        self.current_collation = None;
960        self.commit_state = CommitState::Ready;
961        // Drop any in-flight sequence inner-tx commit-state-machine. If
962        // it was mid-step the inner mv_tx has already been swapped back
963        // (we handle that on every code path inside
964        // `op_sequence_commit_inner_tx`) so dropping the state machine
965        // here only releases its references.
966        self.sequence_inner_commit = None;
967        self.op_vacuum_state = VacuumOpState::None;
968        self.view_delta_state = ViewDeltaCommitState::NotStarted;
969        self.auto_txn_cleanup = TxnCleanup::None;
970        self.fk_immediate_violations_during_stmt
971            .store(0, Ordering::SeqCst);
972        self.fk_deferred_violations_when_stmt_started
973            .store(0, Ordering::SeqCst);
974        self.rowsets.clear();
975        self.bloom_filters.clear();
976        self.hash_tables.clear();
977        self.ephemeral_temp_files.clear();
978        self.uses_subjournal = false;
979        self.is_active_write = false;
980        self.has_stmt_transaction = false;
981        self.distinct_key_values.clear();
982        self.attached_savepoint_pagers.clear();
983        self.n_change.store(0, Ordering::SeqCst);
984        self.n_total_change.store(0, Ordering::SeqCst);
985        *self.explain_state.write() = ExplainState::default();
986        self.pending_fail_error = None;
987        self.pending_cdc_info = None;
988        self.subprogram_stmt_cache.clear();
989    }
990
991    pub(crate) fn record_statement_change(&self) {
992        self.n_change.fetch_add(1, Ordering::SeqCst);
993        self.n_total_change.fetch_add(1, Ordering::SeqCst);
994    }
995
996    pub(crate) fn record_total_change(&self) {
997        self.n_total_change.fetch_add(1, Ordering::SeqCst);
998    }
999
1000    /// Whether this statement may finish the implicit autocommit transaction
1001    /// now, including re-entry while its commit is in progress.
1002    #[inline]
1003    pub(crate) fn can_autocommit_now(&self, connection: &Connection) -> bool {
1004        let is_already_committing = !matches!(self.commit_state, CommitState::Ready);
1005        if is_already_committing {
1006            return true;
1007        }
1008        if self.auto_txn_cleanup != TxnCleanup::RollbackTxn {
1009            return false;
1010        }
1011        let active_writers = connection.n_active_writes.load(Ordering::SeqCst);
1012        turso_assert!(
1013            active_writers <= 1,
1014            "n_active_writes must be 0 or 1, got {active_writers}"
1015        );
1016        if self.is_active_write {
1017            turso_assert!(
1018                active_writers == 1,
1019                "active writer state without an active writer count"
1020            );
1021        }
1022        if connection.mv_store().is_some() {
1023            // MVCC keeps one tx id on the connection. A writer waits for
1024            // sibling readers, and a reader waits for sibling readers/writers.
1025            return connection.n_active_root_statements.load(Ordering::SeqCst) == 1
1026                && (self.is_active_write || active_writers == 0);
1027        }
1028        if self.is_active_write {
1029            // Pager/WAL writers can finish while sibling readers remain active.
1030            // The readers keep their cursors and release them when they finish.
1031            return true;
1032        }
1033        // Pager/WAL readers do not wait for sibling readers.
1034        active_writers == 0
1035    }
1036
1037    #[inline]
1038    pub fn record_rows_read(&mut self, count: u64) {
1039        self.metrics.rows_read = self.metrics.rows_read.saturating_add(count);
1040    }
1041
1042    #[inline]
1043    pub fn record_rows_written(&mut self, count: u64) {
1044        self.metrics.rows_written = self.metrics.rows_written.saturating_add(count);
1045    }
1046
1047    pub(crate) fn metrics(&self) -> StatementMetrics {
1048        let mut metrics = self.metrics.clone();
1049        if let Some(OpProgramState::Step { statement, .. }) = self.active_op_state.program_ref() {
1050            metrics.merge(&statement.metrics());
1051        }
1052        for statement in self.subprogram_stmt_cache.values() {
1053            metrics.merge(&statement.metrics());
1054        }
1055        metrics
1056    }
1057
1058    pub(crate) fn reset_metrics(&mut self) {
1059        self.metrics.reset();
1060        if let Some(OpProgramState::Step { statement, .. }) = self.active_op_state.program_mut() {
1061            statement.reset_metrics();
1062        }
1063        for statement in self.subprogram_stmt_cache.values_mut() {
1064            statement.reset_metrics();
1065        }
1066    }
1067
1068    pub(crate) fn reset_stmt_status(&mut self, counter: crate::statement::StatementStatusCounter) {
1069        match counter {
1070            crate::statement::StatementStatusCounter::FullscanStep => {
1071                self.metrics.fullscan_steps = 0
1072            }
1073            crate::statement::StatementStatusCounter::Sort => self.metrics.sort_operations = 0,
1074            crate::statement::StatementStatusCounter::VmStep => self.metrics.insn_executed = 0,
1075            crate::statement::StatementStatusCounter::Reprepare => self.metrics.reprepares = 0,
1076            crate::statement::StatementStatusCounter::RowsRead => self.metrics.rows_read = 0,
1077            crate::statement::StatementStatusCounter::RowsWritten => self.metrics.rows_written = 0,
1078        }
1079        if let Some(OpProgramState::Step { statement, .. }) = self.active_op_state.program_mut() {
1080            statement.reset_stmt_status(counter);
1081        }
1082        for statement in self.subprogram_stmt_cache.values_mut() {
1083            statement.reset_stmt_status(counter);
1084        }
1085    }
1086
1087    pub fn get_cursor(&mut self, cursor_id: CursorID) -> &mut Cursor {
1088        self.cursors
1089            .get_mut(cursor_id)
1090            .unwrap_or_else(|| panic!("cursor id {cursor_id} out of bounds"))
1091            .as_mut()
1092            .unwrap_or_else(|| panic!("cursor id {cursor_id} is None"))
1093    }
1094
1095    /// Begin a statement subtransaction.
1096    ///
1097    /// Creates a savepoint on the main DB's MvStore (or pager for WAL mode),
1098    /// and snapshots FK violation counters for potential statement rollback.
1099    /// Attached DB savepoints are opened per-DB in `op_transaction_inner`
1100    /// when each DB's Transaction opcode is executed.
1101    ///
1102    /// Pager/MVCC savepoints are only opened for write statements inside an
1103    /// explicit transaction. In autocommit mode, a statement abort is a
1104    /// transaction abort, so savepoints are unnecessary.
1105    pub fn begin_statement(
1106        &mut self,
1107        connection: &Connection,
1108        pager: &Arc<Pager>,
1109        write: bool,
1110    ) -> Result<IOResult<()>> {
1111        let in_explicit_txn = !connection.auto_commit.load(Ordering::SeqCst);
1112        if write && in_explicit_txn {
1113            // Check if MVCC is active - if so, use MVCC savepoints instead of pager savepoints
1114            if let Some(mv_store) = connection.mv_store().as_ref() {
1115                if let Some(tx_id) = connection.get_mv_tx_id() {
1116                    mv_store.begin_savepoint(tx_id);
1117                }
1118            } else {
1119                // Non-MVCC mode: use pager savepoints
1120                let db_size = return_if_io!(pager.with_header(|header| header.database_size.get()));
1121                pager.open_subjournal()?;
1122                pager.try_use_subjournal()?;
1123                let result = pager.open_savepoint(db_size);
1124                if result.is_err() {
1125                    pager.stop_use_subjournal();
1126                }
1127                result?;
1128                self.uses_subjournal = true;
1129            }
1130        }
1131
1132        self.has_stmt_transaction = true;
1133
1134        // Store the deferred foreign key violations counter at the start of the statement.
1135        // This is used to ensure that if an interactive transaction had deferred FK violations and a statement subtransaction rolls back,
1136        // the deferred FK violations are not lost.
1137        self.fk_deferred_violations_when_stmt_started.store(
1138            connection.fk_deferred_violations.load(Ordering::Acquire),
1139            Ordering::SeqCst,
1140        );
1141        // Reset the immediate foreign key violations counter to 0. If this is nonzero when the statement completes, the statement subtransaction will roll back.
1142        self.fk_immediate_violations_during_stmt
1143            .store(0, Ordering::Release);
1144        Ok(IOResult::Done(()))
1145    }
1146
1147    /// End a statement subtransaction.
1148    ///
1149    /// Mirrors SQLite's vdbeCloseStatement (vdbeaux.c:3203-3248). Pager/MVCC
1150    /// savepoint management and FK violation counter restoration are independent
1151    /// concerns: pager savepoints may be skipped (e.g. autocommit optimization)
1152    /// while FK bookkeeping still needs cleanup.
1153    pub fn end_statement(
1154        &mut self,
1155        connection: &Connection,
1156        pager: &Arc<Pager>,
1157        end_statement: EndStatement,
1158    ) -> Result<()> {
1159        if self.is_active_write {
1160            let previous = connection.n_active_writes.fetch_sub(1, Ordering::SeqCst);
1161            turso_assert!(
1162                previous == 1,
1163                "ending a writer with {previous} active writer(s)"
1164            );
1165            self.is_active_write = false;
1166        }
1167        // If begin_statement was never called, no savepoint/FK cleanup needed.
1168        if !self.has_stmt_transaction {
1169            return Ok(());
1170        }
1171        self.has_stmt_transaction = false;
1172
1173        // Drain attached pagers upfront so we can clean them up regardless of path.
1174        let attached_pagers: Vec<Arc<Pager>> = self.attached_savepoint_pagers.drain(..).collect();
1175        let result = match end_statement {
1176            EndStatement::ReleaseSavepoint => {
1177                if let Some(mv_store) = connection.mv_store().as_ref() {
1178                    if let Some(tx_id) = connection.get_mv_tx_id() {
1179                        mv_store.release_savepoint(tx_id);
1180                    }
1181                    connection.for_each_attached_mv_tx(|db_id, tx_id| {
1182                        if let Some(attached_mv) = connection.mv_store_for_db(db_id) {
1183                            attached_mv.release_savepoint(tx_id);
1184                        }
1185                    });
1186                    Ok(())
1187                } else if self.uses_subjournal || !attached_pagers.is_empty() {
1188                    if self.uses_subjournal {
1189                        pager.release_savepoint()?;
1190                    }
1191                    for p in &attached_pagers {
1192                        p.release_savepoint()?;
1193                    }
1194                    Ok(())
1195                } else {
1196                    Ok(())
1197                }
1198            }
1199            EndStatement::RollbackSavepoint => {
1200                // Rollback pager/MVCC savepoint if one was opened.
1201                let pager_err = if let Some(mv_store) = connection.mv_store().as_ref() {
1202                    let mut err = None;
1203                    if let Some(tx_id) = connection.get_mv_tx_id() {
1204                        if let Err(e) = mv_store.rollback_first_savepoint(tx_id) {
1205                            err = Some(e);
1206                        }
1207                    }
1208                    connection.for_each_attached_mv_tx(|db_id, tx_id| {
1209                        if let Some(attached_mv) = connection.mv_store_for_db(db_id) {
1210                            if let Err(e) = attached_mv.rollback_first_savepoint(tx_id) {
1211                                if err.is_none() {
1212                                    err = Some(e);
1213                                }
1214                            }
1215                        }
1216                    });
1217                    err
1218                } else if self.uses_subjournal {
1219                    match pager.rollback_to_newest_savepoint() {
1220                        Ok(_) => {
1221                            let mut err = None;
1222                            for p in &attached_pagers {
1223                                if let Err(e) = p.rollback_to_newest_savepoint() {
1224                                    err = Some(e);
1225                                    break;
1226                                }
1227                            }
1228                            err
1229                        }
1230                        Err(e) => Some(e),
1231                    }
1232                } else if !attached_pagers.is_empty() {
1233                    let mut err = None;
1234                    for p in &attached_pagers {
1235                        if let Err(e) = p.rollback_to_newest_savepoint() {
1236                            err = Some(e);
1237                        }
1238                    }
1239                    err
1240                } else {
1241                    None
1242                };
1243
1244                // Always restore FK violation counters on statement rollback,
1245                // regardless of whether a pager savepoint was opened.
1246                // Mirrors SQLite's vdbeCloseStatement (vdbeaux.c:3243-3246).
1247                connection.fk_deferred_violations.store(
1248                    self.fk_deferred_violations_when_stmt_started
1249                        .load(Ordering::Acquire),
1250                    Ordering::SeqCst,
1251                );
1252
1253                match pager_err {
1254                    Some(e) => Err(e),
1255                    None => Ok(()),
1256                }
1257            }
1258        };
1259        if self.uses_subjournal {
1260            pager.stop_use_subjournal();
1261            self.uses_subjournal = false;
1262        }
1263        for p in &attached_pagers {
1264            p.stop_use_subjournal();
1265        }
1266        result
1267    }
1268
1269    /// Gets or creates a bloom filter for the given cursor ID.
1270    pub fn get_or_create_bloom_filter(&mut self, cursor_id: usize) -> &mut BloomFilter {
1271        self.bloom_filters.entry(cursor_id).or_default()
1272    }
1273
1274    /// Gets or creates a bloom filter with a specific capacity for the given cursor ID.
1275    pub fn get_or_create_bloom_filter_with_capacity(
1276        &mut self,
1277        cursor_id: usize,
1278        expected_items: u32,
1279        false_positive_rate: f32,
1280    ) -> &mut BloomFilter {
1281        self.bloom_filters
1282            .entry(cursor_id)
1283            .or_insert_with(|| BloomFilter::with_capacity(expected_items, false_positive_rate))
1284    }
1285
1286    /// Gets an existing bloom filter for the given cursor ID.
1287    pub fn get_bloom_filter(&self, cursor_id: usize) -> Option<&BloomFilter> {
1288        self.bloom_filters.get(&cursor_id)
1289    }
1290
1291    /// Gets a mutable reference to an existing bloom filter for the given cursor ID.
1292    pub fn get_bloom_filter_mut(&mut self, cursor_id: usize) -> Option<&mut BloomFilter> {
1293        self.bloom_filters.get_mut(&cursor_id)
1294    }
1295
1296    /// Removes and drops the bloom filter for the given cursor ID.
1297    pub fn remove_bloom_filter(&mut self, cursor_id: usize) {
1298        self.bloom_filters.remove(&cursor_id);
1299    }
1300
1301    /// Checks if a bloom filter exists for the given cursor ID.
1302    pub fn has_bloom_filter(&self, cursor_id: usize) -> bool {
1303        self.bloom_filters.contains_key(&cursor_id)
1304    }
1305
1306    pub fn get_fk_immediate_violations_during_stmt(&self) -> isize {
1307        self.fk_immediate_violations_during_stmt
1308            .load(Ordering::Acquire)
1309    }
1310
1311    pub fn increment_fk_immediate_violations_during_stmt(&self, v: isize) {
1312        self.fk_immediate_violations_during_stmt
1313            .fetch_add(v, Ordering::AcqRel);
1314    }
1315}
1316
1317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1318/// Action to take at the end of a statement subtransaction.
1319pub enum EndStatement {
1320    /// Release (commit) the savepoint -- effectively removing the savepoint as it is no longer needed for undo purposes.
1321    ReleaseSavepoint,
1322    /// Rollback (abort) to the newest savepoint: read pages from the subjournal and restore them to the page cache.
1323    /// This is used to undo the changes made by the statement.
1324    RollbackSavepoint,
1325}
1326
1327impl Register {
1328    pub fn get_value(&self) -> &Value {
1329        match self {
1330            Register::Value(v) => v,
1331            Register::Record(r) => {
1332                turso_assert!(!r.is_invalidated());
1333                r.as_blob_value()
1334            }
1335            _ => panic!("register holds unexpected value: {self:?}"),
1336        }
1337    }
1338}
1339
1340#[macro_export]
1341macro_rules! must_be_btree_cursor {
1342    ($cursor_id:expr, $cursor_ref:expr, $state:expr, $insn_name:expr) => {{
1343        let (_, cursor_type) = $cursor_ref.get($cursor_id).unwrap();
1344        if matches!(
1345            cursor_type,
1346            CursorType::BTreeTable(_)
1347                | CursorType::BTreeIndex(_)
1348                | CursorType::MaterializedView(_, _)
1349        ) {
1350            $crate::get_cursor!($state, $cursor_id)
1351        } else {
1352            panic!("{} on unexpected cursor", $insn_name)
1353        }
1354    }};
1355}
1356
1357/// Macro is necessary to help the borrow checker see we are only accessing state.cursor field
1358/// and nothing else
1359#[macro_export]
1360macro_rules! get_cursor {
1361    ($state:expr, $cursor_id:expr) => {
1362        $state
1363            .cursors
1364            .get_mut($cursor_id)
1365            .unwrap_or_else(|| panic!("cursor id {} out of bounds", $cursor_id))
1366            .as_mut()
1367            .unwrap_or_else(|| panic!("cursor id {} is None", $cursor_id))
1368    };
1369}
1370
1371/// Tracks the state of explain mode execution, including which subprograms need to be processed.
1372#[derive(Default)]
1373pub struct ExplainState {
1374    /// Subprograms queued for explain output, processed after the parent program finishes.
1375    pending: std::collections::VecDeque<Arc<PreparedProgram>>,
1376    /// Prepared subprograms that have already been queued for explain output.
1377    ///
1378    /// Recursive foreign-key action programs can contain a `Program` instruction
1379    /// that calls the same prepared program again. Without this set, EXPLAIN
1380    /// keeps printing the same subprogram forever.
1381    queued_subprograms: std::collections::HashSet<usize>,
1382    /// The subprogram currently being explained, if any.
1383    current: Option<Arc<PreparedProgram>>,
1384}
1385
1386impl ExplainState {
1387    /// Queue a subprogram for EXPLAIN output if this statement has not queued it before.
1388    fn queue_subprogram_once(&mut self, subprogram: Arc<PreparedProgram>) {
1389        let subprogram_id = Arc::as_ptr(&subprogram) as usize;
1390        if self.queued_subprograms.insert(subprogram_id) {
1391            self.pending.push_back(subprogram);
1392        }
1393    }
1394}
1395
1396#[derive(Debug, Clone)]
1397pub struct PreparedProgram {
1398    pub max_registers: usize,
1399    // we store original indices because we don't want to create new vec from
1400    // ProgramBuilder
1401    pub insns: Vec<(Insn, usize)>,
1402    pub cursor_ref: Vec<(Option<CursorKey>, CursorType)>,
1403    pub comments: Vec<(InsnReference, &'static str)>,
1404    pub parameters: crate::parameters::Parameters,
1405    pub change_cnt_on: bool,
1406    /// Flag that detect if the sqlite statement will directly manipulate the database file.\
1407    /// mirrors: https://sqlite.org/c3ref/stmt_readonly.html.
1408    pub readonly: bool,
1409    pub result_columns: Vec<ResultSetColumn>,
1410    pub table_references: TableReferences,
1411    pub sql: String,
1412    /// Whether the statement needs to be wrapped in a statement subtransaction
1413    /// when run as part of an interactive (non-autocommit) transaction.
1414    /// See [crate::vdbe::builder::ProgramBuilder::is_multi_write] and [crate::vdbe::builder::ProgramBuilder::may_abort] for more details.
1415    pub needs_stmt_subtransactions: Arc<AtomicBool>,
1416    /// If this Program is a trigger subprogram, a ref to the trigger is stored here.
1417    pub trigger: Option<Arc<Trigger>>,
1418    /// Whether this program is a subprogram (trigger or FK action) that runs within a parent statement.
1419    pub is_subprogram: bool,
1420    pub resolve_type: ResolveType,
1421    pub prepare_context: PrepareContext,
1422    /// Set of attached database indices that need write transactions.
1423    pub write_databases: BitSet,
1424    /// Set of attached database indices that need read transactions.
1425    pub read_databases: BitSet,
1426}
1427
1428#[derive(Clone)]
1429pub struct Program {
1430    pub(crate) prepared: Arc<PreparedProgram>,
1431    pub connection: Arc<Connection>,
1432}
1433
1434/// Captures connection settings at statement preparation time for cache invalidation.
1435///
1436/// This struct is used to detect when a cached prepared statement needs to be recompiled
1437/// because relevant connection settings have changed. When `matches_connection()` returns
1438/// false, the statement will be automatically reprepared before execution.
1439///
1440/// # Adding New Fields
1441///
1442/// If you add a new setting to `Connection` that affects statement compilation or execution,
1443/// When adding a new connection setting that affects query compilation, you MUST call
1444/// `bump_prepare_context_generation()` in its setter so that prepared statements know
1445/// they need to be reprepared.
1446#[derive(Debug, Clone, PartialEq, Eq)]
1447pub struct PrepareContext {
1448    /// Identity check: the prepared statement must belong to the same database.
1449    database_ptr: usize,
1450    /// Generation counter snapshot taken at prepare time. Compared against the
1451    /// connection's current generation to detect setting changes (pragmas,
1452    /// attach/detach, extension registration, etc.) without rebuilding the full
1453    /// context on every step.
1454    generation: u64,
1455}
1456
1457impl PrepareContext {
1458    pub fn from_connection(connection: &Connection) -> Self {
1459        Self {
1460            database_ptr: connection.database_ptr(),
1461            generation: connection.prepare_context_generation(),
1462        }
1463    }
1464
1465    #[inline]
1466    pub fn matches_connection(&self, connection: &Connection) -> bool {
1467        self.database_ptr == connection.database_ptr()
1468            && self.generation == connection.prepare_context_generation()
1469    }
1470}
1471
1472impl PreparedProgram {
1473    pub fn bind(self: Arc<Self>, connection: Arc<Connection>) -> Program {
1474        Program {
1475            prepared: self,
1476            connection,
1477        }
1478    }
1479
1480    pub fn is_compatible_with(&self, connection: &Connection) -> bool {
1481        self.prepare_context.matches_connection(connection)
1482    }
1483
1484    #[inline]
1485    pub const fn is_readonly(&self) -> bool {
1486        self.readonly
1487    }
1488}
1489
1490impl Program {
1491    #[inline]
1492    pub fn prepared(&self) -> &Arc<PreparedProgram> {
1493        &self.prepared
1494    }
1495
1496    pub fn from_prepared(prepared: Arc<PreparedProgram>, connection: Arc<Connection>) -> Self {
1497        Self {
1498            prepared,
1499            connection,
1500        }
1501    }
1502
1503    #[inline]
1504    pub fn is_readonly(&self) -> bool {
1505        self.prepared().is_readonly()
1506    }
1507}
1508
1509impl Program {
1510    fn get_pager_from_database_index(&self, idx: &usize) -> Result<Arc<Pager>> {
1511        self.connection.get_pager_from_database_index(idx)
1512    }
1513
1514    #[inline]
1515    fn maybe_request_interrupt<I>(&self, state: &mut ProgramState, io: &I) -> bool
1516    where
1517        I: crate::IO + ?Sized,
1518    {
1519        let connection_interrupt = self.connection.is_interrupted();
1520        let hit_query_deadline = state
1521            .query_deadline
1522            .is_some_and(|deadline| io.current_time_monotonic() >= deadline);
1523        let progress_interrupt = self
1524            .connection
1525            .should_interrupt_for_progress(state.metrics.vm_steps);
1526        if connection_interrupt || hit_query_deadline || progress_interrupt {
1527            state.interrupt();
1528        }
1529        state.is_interrupted()
1530    }
1531
1532    #[turso_macros::trace_stack]
1533    pub fn step(
1534        &self,
1535        state: &mut ProgramState,
1536        pager: &Arc<Pager>,
1537        query_mode: QueryMode,
1538        waker: Option<&Waker>,
1539    ) -> Result<StepResult> {
1540        state.execution_state = ProgramExecutionState::Running;
1541        let result = match query_mode {
1542            QueryMode::Normal => self.normal_step(state, pager, waker),
1543            QueryMode::Explain => self.explain_step(state, pager),
1544            QueryMode::ExplainQueryPlan => self.explain_query_plan_step(state, pager),
1545        };
1546        match &result {
1547            Ok(StepResult::Done) => {
1548                state.execution_state = ProgramExecutionState::Done;
1549            }
1550            Ok(StepResult::Interrupt) => {
1551                state.execution_state = ProgramExecutionState::Interrupted;
1552            }
1553            Err(_) => {
1554                state.execution_state = ProgramExecutionState::Failed;
1555            }
1556            _ => {}
1557        }
1558        result
1559    }
1560
1561    fn explain_step(&self, state: &mut ProgramState, pager: &Arc<Pager>) -> Result<StepResult> {
1562        turso_debug_assert!(state.column_count() == EXPLAIN_COLUMNS.len());
1563        if self.connection.is_closed() {
1564            let tx_state = self.connection.get_tx_state();
1565            if let TransactionState::Write { .. } = tx_state {
1566                pager.rollback_tx(&self.connection);
1567            }
1568            return Err(LimboError::InternalError("Connection closed".to_string()));
1569        }
1570
1571        if self.maybe_request_interrupt(state, pager.io.as_ref()) {
1572            return Ok(StepResult::Interrupt);
1573        }
1574
1575        state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1);
1576
1577        let mut explain_state = state.explain_state.write();
1578
1579        // Advance to the next subprogram if the current one is finished
1580        loop {
1581            if let Some(ref current) = explain_state.current {
1582                if (state.pc as usize) < current.insns.len() {
1583                    break;
1584                }
1585            } else if (state.pc as usize) < self.insns.len() {
1586                break;
1587            }
1588            // Current program is done, pop next subprogram from queue
1589            if let Some(next) = explain_state.pending.pop_front() {
1590                explain_state.current = Some(next);
1591                state.pc = 0;
1592            } else {
1593                explain_state.current = None;
1594                return Ok(StepResult::Done);
1595            }
1596        }
1597
1598        let pc = state.pc as usize;
1599
1600        // Explain the current instruction from the active program.
1601        // We collect subprograms separately to avoid borrow conflicts with explain_state.
1602        let (row, subprogram) = if let Some(ref current) = explain_state.current {
1603            let (insn, _) = &current.insns[pc];
1604            let sub = if let Insn::Program {
1605                program: subprogram,
1606                ..
1607            } = insn
1608            {
1609                Some(subprogram.prepared_program()?)
1610            } else {
1611                None
1612            };
1613            let comment = current
1614                .comments
1615                .iter()
1616                .find(|(offset, _)| *offset == state.pc)
1617                .map(|(_, c)| *c);
1618            (insn_to_row_with_comment(current, insn, comment), sub)
1619        } else {
1620            let (insn, _) = &self.insns[pc];
1621            let sub = if let Insn::Program {
1622                program: subprogram,
1623                ..
1624            } = insn
1625            {
1626                Some(subprogram.prepared_program()?)
1627            } else {
1628                None
1629            };
1630            let comment = self
1631                .comments
1632                .iter()
1633                .find(|(offset, _)| *offset == state.pc)
1634                .map(|(_, c)| *c);
1635            (insn_to_row_with_comment(self, insn, comment), sub)
1636        };
1637        if let Some(sub) = subprogram {
1638            explain_state.queue_subprogram_once(sub);
1639        }
1640        let (opcode, p1, p2, p3, p4, p5, comment) = row;
1641
1642        state.registers[0].set_int(state.pc as i64);
1643        state.registers[1].set_value(Value::from_text(opcode));
1644        state.registers[2].set_int(p1);
1645        state.registers[3].set_int(p2);
1646        state.registers[4].set_int(p3);
1647        state.registers[5].set_value(p4);
1648        state.registers[6].set_int(p5);
1649        state.registers[7].set_value(Value::from_text(comment));
1650        state.result_row = Some(Row {
1651            values: &state.registers[0] as *const Register,
1652            count: EXPLAIN_COLUMNS.len(),
1653        });
1654        state.pc += 1;
1655        Ok(StepResult::Row)
1656    }
1657
1658    fn explain_query_plan_step(
1659        &self,
1660        state: &mut ProgramState,
1661        pager: &Arc<Pager>,
1662    ) -> Result<StepResult> {
1663        turso_debug_assert!(state.column_count() == EXPLAIN_QUERY_PLAN_COLUMNS.len());
1664        loop {
1665            if self.connection.is_closed() {
1666                // Connection is closed for whatever reason, rollback the transaction.
1667                let state = self.connection.get_tx_state();
1668                if let TransactionState::Write { .. } = state {
1669                    pager.rollback_tx(&self.connection);
1670                }
1671                return Err(LimboError::InternalError("Connection closed".to_string()));
1672            }
1673
1674            if self.maybe_request_interrupt(state, pager.io.as_ref()) {
1675                return Ok(StepResult::Interrupt);
1676            }
1677
1678            // FIXME: do we need this?
1679            state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1);
1680
1681            if state.pc as usize >= self.insns.len() {
1682                return Ok(StepResult::Done);
1683            }
1684
1685            let Insn::Explain { p1, p2, detail } = &self.insns[state.pc as usize].0 else {
1686                state.pc += 1;
1687                continue;
1688            };
1689
1690            state.registers[0].set_int(*p1 as i64);
1691            state.registers[1] =
1692                Register::Value(Value::from_i64(p2.as_ref().map(|p| *p).unwrap_or(0) as i64));
1693            state.registers[2].set_int(0);
1694            state.registers[3].set_value(Value::from_text(detail.clone()));
1695            state.result_row = Some(Row {
1696                values: &state.registers[0] as *const Register,
1697                count: EXPLAIN_QUERY_PLAN_COLUMNS.len(),
1698            });
1699            state.pc += 1;
1700            return Ok(StepResult::Row);
1701        }
1702    }
1703
1704    #[instrument(skip_all, level = Level::DEBUG)]
1705    fn normal_step(
1706        &self,
1707        state: &mut ProgramState,
1708        pager: &Arc<Pager>,
1709        waker: Option<&Waker>,
1710    ) -> Result<StepResult> {
1711        let enable_tracing = tracing::enabled!(tracing::Level::TRACE);
1712        loop {
1713            if self.connection.is_closed() {
1714                // Connection is closed for whatever reason, rollback the transaction.
1715                let state = self.connection.get_tx_state();
1716                if let TransactionState::Write { .. } = state {
1717                    pager.rollback_tx(&self.connection);
1718                }
1719                return Err(LimboError::InternalError("Connection closed".to_string()));
1720            }
1721            if self.maybe_request_interrupt(state, pager.io.as_ref()) {
1722                self.abort(pager, None, state)?;
1723                return Ok(StepResult::Interrupt);
1724            }
1725
1726            if let Some(io) = &state.io_completions {
1727                if !io.finished() {
1728                    io.set_waker(waker);
1729                    return Ok(StepResult::IO);
1730                }
1731                if let Some(err) = io.get_error() {
1732                    if pager.is_checkpointing() {
1733                        // Wrap IO errors that occurred during checkpointing in CheckpointFailed error,
1734                        // so that abort() knows not to try to rollback the transaction, because the transaction
1735                        // is already durable in the WAL and hence committed.
1736                        // This also lets the simulator know that it should shadow the results of the query because
1737                        // the write itself succeeded.
1738                        let checkpoint_err = LimboError::CheckpointFailed(err.to_string());
1739                        tracing::error!("Checkpoint failed: {checkpoint_err}");
1740                        if let Err(abort_err) = self.abort(pager, Some(&checkpoint_err), state) {
1741                            tracing::error!(
1742                                "Abort also failed during checkpoint error handling: {abort_err}"
1743                            );
1744                        }
1745                        return Err(checkpoint_err);
1746                    }
1747                    let err = err.into();
1748                    if let Err(abort_err) = self.abort(pager, Some(&err), state) {
1749                        tracing::error!("Abort failed during error handling: {abort_err}");
1750                    }
1751                    return Err(err);
1752                }
1753                state.io_completions = None;
1754            }
1755            // invalidate row
1756            let _ = state.result_row.take();
1757            let (insn, _) = &self.insns[state.pc as usize];
1758            let insn_function = insn.to_function();
1759            if enable_tracing {
1760                trace_insn(self, state.pc as InsnReference, insn);
1761                crate::stack::trace_remaining("program_step:opcode");
1762            }
1763            if self.connection.get_vdbe_trace() {
1764                // Diff registers from PREVIOUS opcode
1765                // The last opcode (Halt) won't have its diff printed, but Halt
1766                // doesn't write to any registers
1767                if let Some(ref old) = state.pre_op_registers {
1768                    for (i, (old_reg, new_reg)) in
1769                        old.iter().zip(state.registers.iter()).enumerate()
1770                    {
1771                        if old_reg != new_reg {
1772                            match new_reg {
1773                                Register::Value(v) => eprintln!("R[{i}] = {v}"),
1774                                Register::Aggregate(_) => eprintln!("R[{i}] = <aggregate>"),
1775                                Register::Record(_) => eprintln!("R[{i}] = <record>"),
1776                            }
1777                        }
1778                    }
1779                    state.pre_op_registers = None;
1780                }
1781
1782                // Print CURRENT opcode
1783                if matches!(insn, Insn::Init { .. }) {
1784                    eprintln!("VDBE Trace:");
1785                }
1786                eprintln!(
1787                    "{}",
1788                    explain::insn_to_str(
1789                        self,
1790                        state.pc as InsnReference,
1791                        insn,
1792                        String::new(),
1793                        self.comments
1794                            .iter()
1795                            .find(|(offset, _)| *offset == state.pc as InsnReference)
1796                            .map(|(_, comment)| comment)
1797                            .copied()
1798                    )
1799                );
1800                // Snapshot for next iteration
1801                state.pre_op_registers = Some(state.registers.clone());
1802            }
1803            // Always increment VM steps for every loop iteration
1804            state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1);
1805
1806            match insn_function(self, state, insn, pager) {
1807                Ok(InsnFunctionStepResult::Step) => {
1808                    // Instruction completed, moving to next
1809                    state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1);
1810                }
1811                Ok(InsnFunctionStepResult::Done) => {
1812                    // Instruction completed execution
1813                    state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1);
1814                    state.auto_txn_cleanup = TxnCleanup::None;
1815                    return Ok(StepResult::Done);
1816                }
1817                Ok(InsnFunctionStepResult::IO(io)) => {
1818                    // Instruction not complete - waiting for I/O, will resume at same PC
1819                    io.set_waker(waker);
1820                    let is_yield = io.is_explicit_yield();
1821                    if is_yield {
1822                        // Yield: return control to the cooperative scheduler so
1823                        // other connections can make progress (e.g. release a
1824                        // contended lock). Don't store in io_completions —
1825                        // yields aren't pending I/O, so the instruction will
1826                        // simply re-execute on the next step.
1827                        return Ok(StepResult::Yield);
1828                    }
1829                    let finished = io.finished();
1830                    state.io_completions = Some(io);
1831                    if !finished {
1832                        return Ok(StepResult::IO);
1833                    }
1834                    // just continue the outer loop if IO is finished so db will continue execution immediately
1835                }
1836                Ok(InsnFunctionStepResult::Row) => {
1837                    // Instruction completed (ResultRow already incremented PC)
1838                    state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1);
1839                    return Ok(StepResult::Row);
1840                }
1841                Err(LimboError::Busy) => {
1842                    // Instruction blocked - will retry at same PC
1843                    return Ok(StepResult::Busy);
1844                }
1845                Err(LimboError::BusySnapshot)
1846                    if self.connection.transaction_state.get() == TransactionState::None =>
1847                {
1848                    // For interactive transactions that are already in a read transaction, retrying BusySnapshot is pointless
1849                    // because the snapshot will continue to be stale no matter how many times we retry.
1850                    // However, for auto-commits or BEGIN IMMEDIATE, failing to promote to write transaction means it was rolled
1851                    // back, so auto-retrying can be useful.
1852                    return Ok(StepResult::Busy);
1853                }
1854                Err(err) => {
1855                    if let Err(abort_err) = self.abort(pager, Some(&err), state) {
1856                        tracing::error!("Abort failed during error handling: {abort_err}");
1857                    }
1858                    return Err(err);
1859                }
1860            }
1861        }
1862    }
1863
1864    #[instrument(skip_all, level = Level::DEBUG)]
1865    fn apply_view_deltas(
1866        &self,
1867        state: &mut ProgramState,
1868        rollback: bool,
1869        pager: &Arc<Pager>,
1870    ) -> Result<IOResult<()>> {
1871        use crate::types::IOResult;
1872
1873        loop {
1874            match &state.view_delta_state {
1875                ViewDeltaCommitState::NotStarted => {
1876                    if self.connection.view_transaction_states.is_empty() {
1877                        return Ok(IOResult::Done(()));
1878                    }
1879
1880                    if rollback {
1881                        // On rollback, just clear and done
1882                        self.connection.view_transaction_states.clear();
1883                        return Ok(IOResult::Done(()));
1884                    }
1885
1886                    // Not a rollback - proceed with processing
1887                    let schema = self.connection.schema.read();
1888
1889                    // Collect materialized views - they should all have storage
1890                    let mut views = Vec::new();
1891                    for view_name in self.connection.view_transaction_states.get_view_names() {
1892                        if let Some(view_mutex) = schema.get_materialized_view(&view_name) {
1893                            let view = view_mutex.lock();
1894                            let root_page = view.get_root_page();
1895
1896                            // Materialized views should always have storage (root_page != 0)
1897                            turso_assert_ne!(
1898                                root_page, 0,
1899                                "Materialized view should have a root page",
1900                                { "view_name": view_name }
1901                            );
1902
1903                            views.push(view_name);
1904                        }
1905                    }
1906
1907                    state.view_delta_state = ViewDeltaCommitState::Processing {
1908                        views,
1909                        current_index: 0,
1910                    };
1911                }
1912
1913                ViewDeltaCommitState::Processing {
1914                    views,
1915                    current_index,
1916                } => {
1917                    // At this point we know it's not a rollback
1918                    if *current_index >= views.len() {
1919                        // All done, clear the transaction states
1920                        self.connection.view_transaction_states.clear();
1921                        state.view_delta_state = ViewDeltaCommitState::Done;
1922                        return Ok(IOResult::Done(()));
1923                    }
1924
1925                    let view_name = &views[*current_index];
1926
1927                    let table_deltas = self
1928                        .connection
1929                        .view_transaction_states
1930                        .get(view_name)
1931                        .expect("view should have transaction state")
1932                        .get_table_deltas();
1933
1934                    let schema = self.connection.schema.read();
1935                    if let Some(view_mutex) = schema.get_materialized_view(view_name) {
1936                        let mut view = view_mutex.lock();
1937
1938                        // Create a DeltaSet from the per-table deltas
1939                        let mut delta_set = crate::incremental::compiler::DeltaSet::new();
1940                        for (table_name, delta) in table_deltas {
1941                            delta_set.insert(table_name, delta);
1942                        }
1943
1944                        // Handle I/O from merge_delta - pass pager, circuit will create its own cursor
1945                        match view.merge_delta(delta_set, pager.clone())? {
1946                            IOResult::Done(_) => {
1947                                // Move to next view
1948                                state.view_delta_state = ViewDeltaCommitState::Processing {
1949                                    views: views.clone(),
1950                                    current_index: current_index + 1,
1951                                };
1952                            }
1953                            IOResult::IO(io) => {
1954                                // Return I/O, will resume at same index
1955                                return Ok(IOResult::IO(io));
1956                            }
1957                        }
1958                    }
1959                }
1960
1961                ViewDeltaCommitState::Done => {
1962                    return Ok(IOResult::Done(()));
1963                }
1964            }
1965        }
1966    }
1967
1968    pub fn commit_txn(
1969        &self,
1970        pager: Arc<Pager>,
1971        program_state: &mut ProgramState,
1972        mv_store: Option<&Arc<MvStore>>,
1973        rollback: bool,
1974    ) -> Result<IOResult<()>> {
1975        // Apply view deltas with I/O handling
1976        match self.apply_view_deltas(program_state, rollback, &pager)? {
1977            IOResult::IO(io) => return Ok(IOResult::IO(io)),
1978            IOResult::Done(_) => {}
1979        }
1980
1981        // Reset state for next use
1982        program_state.view_delta_state = ViewDeltaCommitState::NotStarted;
1983        let tx_state = self.connection.get_tx_state();
1984        if tx_state == TransactionState::None
1985            && matches!(program_state.commit_state, CommitState::Ready)
1986        {
1987            // No main transaction and no in-progress commit — check whether
1988            // any attached/temp database still has an active transaction before
1989            // bailing out. Defer these checks to here so the common case
1990            // (active main transaction) doesn't pay for the lock reads.
1991            let has_attached_mv_tx = self.connection.next_attached_mv_tx().is_some();
1992            let has_attached_wal_tx =
1993                self.connection
1994                    .with_all_attached_pagers_with_index(|pagers| {
1995                        pagers.iter().any(|(_, pager)| pager.holds_read_lock())
1996                    });
1997            if !has_attached_mv_tx && !has_attached_wal_tx {
1998                return Ok(IOResult::Done(()));
1999            }
2000        }
2001        if self.connection.is_nested_stmt() {
2002            // We don't want to commit on nested statements. Let parent handle it.
2003            return Ok(IOResult::Done(()));
2004        }
2005        let res = if let Some(mv_store) = mv_store {
2006            self.commit_txn_mvcc(pager, program_state, mv_store, rollback)
2007        } else {
2008            self.commit_txn_wal(pager, program_state, rollback)
2009        }?;
2010        if !res.is_io() {
2011            if self.change_cnt_on {
2012                self.connection
2013                    .set_changes(program_state.n_change.load(Ordering::SeqCst));
2014                self.connection
2015                    .add_total_changes(program_state.n_total_change.load(Ordering::SeqCst));
2016            }
2017            let transaction_finished = self.connection.auto_commit.load(Ordering::SeqCst)
2018                && self.connection.get_tx_state() == TransactionState::None;
2019            if transaction_finished {
2020                // Finalize the in-memory TEMP schema only when the outer
2021                // transaction actually finishes. Updating the committed temp
2022                // snapshot after every statement inside an explicit
2023                // transaction would make a later full ROLLBACK restore
2024                // uncommitted temp DDL.
2025                if rollback {
2026                    self.connection.rollback_temp_schema();
2027                } else {
2028                    self.connection.commit_temp_schema();
2029                }
2030            }
2031        }
2032        Ok(res)
2033    }
2034
2035    fn commit_txn_wal(
2036        &self,
2037        pager: Arc<Pager>,
2038        program_state: &mut ProgramState,
2039        rollback: bool,
2040    ) -> Result<IOResult<()>> {
2041        let connection = self.connection.clone();
2042        let auto_commit = connection.auto_commit.load(Ordering::SeqCst);
2043        let tx_state = connection.get_tx_state();
2044        tracing::debug!(
2045            "Halt auto_commit {}, commit_state={:?}, tx_state={:?}",
2046            auto_commit,
2047            program_state.commit_state,
2048            tx_state,
2049        );
2050        if matches!(program_state.commit_state, CommitState::Committing) {
2051            let TransactionState::Write { .. } = tx_state else {
2052                unreachable!("invalid state for write commit step")
2053            };
2054            self.step_end_write_txn(&pager, &connection, program_state, rollback)
2055        } else if matches!(program_state.commit_state, CommitState::CommittingAttached) {
2056            // Re-entry after IO yield from attached pager commit.
2057            match self.end_attached_write_txns(&connection, rollback)? {
2058                IOResult::Done(_) => {
2059                    program_state.commit_state = CommitState::Ready;
2060                    if pager.holds_read_lock() {
2061                        pager.end_read_tx();
2062                    }
2063                    self.end_attached_read_txns(&connection);
2064                    Ok(IOResult::Done(()))
2065                }
2066                IOResult::IO(io) => Ok(IOResult::IO(io)),
2067            }
2068        } else if auto_commit {
2069            match tx_state {
2070                TransactionState::Write { .. } => {
2071                    self.step_end_write_txn(&pager, &connection, program_state, rollback)
2072                }
2073                TransactionState::Read => {
2074                    connection.set_tx_state(TransactionState::None);
2075                    // Commit any attached write transactions that were opened
2076                    // independently of the main connection's transaction state.
2077                    // (e.g., UPDATE aux0.t SET ... only needs Read on main DB
2078                    // but holds a write lock on the attached pager.)
2079                    match self.end_attached_write_txns(&connection, rollback)? {
2080                        IOResult::Done(_) => {}
2081                        IOResult::IO(io) => {
2082                            program_state.commit_state = CommitState::CommittingAttached;
2083                            return Ok(IOResult::IO(io));
2084                        }
2085                    }
2086                    pager.end_read_tx();
2087                    self.end_attached_read_txns(&connection);
2088                    Ok(IOResult::Done(()))
2089                }
2090                TransactionState::None => {
2091                    match self.end_attached_write_txns(&connection, rollback)? {
2092                        IOResult::Done(_) => {}
2093                        IOResult::IO(io) => {
2094                            program_state.commit_state = CommitState::CommittingAttached;
2095                            return Ok(IOResult::IO(io));
2096                        }
2097                    }
2098                    self.end_attached_read_txns(&connection);
2099                    Ok(IOResult::Done(()))
2100                }
2101                TransactionState::PendingUpgrade { .. } => {
2102                    panic!("Unexpected transaction state: {tx_state:?} during auto-commit",)
2103                }
2104            }
2105        } else {
2106            Ok(IOResult::Done(()))
2107        }
2108    }
2109
2110    /// Commit MVCC transactions across all databases in a multi-phase protocol:
2111    ///
2112    /// 1. **Main DB MVCC** — commit the main database's MvStore transaction.
2113    /// 2. **Attached MVCC** — commit each attached database's MvStore transaction.
2114    /// 3. **Attached WAL** — flush dirty pages on attached databases that use WAL
2115    ///    (e.g. :memory: attached while main is MVCC).
2116    ///
2117    /// **IMPORTANT**: This multi-phase commit is NOT atomic across databases.
2118    /// A crash between phases can leave the main and attached databases in
2119    /// inconsistent states (main committed, some attached DBs not committed).
2120    /// This matches SQLite's WAL mode behavior — cross-file atomicity only
2121    /// exists in legacy rollback journal mode, which we do not support.
2122    fn commit_txn_mvcc(
2123        &self,
2124        pager: Arc<Pager>,
2125        program_state: &mut ProgramState,
2126        mv_store: &Arc<MvStore>,
2127        rollback: bool,
2128    ) -> Result<IOResult<()>> {
2129        let conn = self.connection.clone();
2130        let auto_commit = conn.auto_commit.load(Ordering::SeqCst);
2131        if !auto_commit {
2132            return Ok(IOResult::Done(()));
2133        }
2134
2135        // Phase 1: Commit main DB MVCC transaction
2136        if matches!(program_state.commit_state, CommitState::Ready) {
2137            if let Some(tx_id) = conn.get_mv_tx_id() {
2138                let state_machine = mv_store.commit_tx(tx_id, &conn, crate::MAIN_DB_ID)?;
2139                program_state.commit_state = CommitState::CommittingMvcc { state_machine };
2140            }
2141            // If no main MVCC tx, commit_state stays Ready and we fall
2142            // through directly to phase 2 (the CommittingMvcc and
2143            // CommittingAttachedMvcc checks will both miss).
2144        }
2145
2146        if matches!(
2147            program_state.commit_state,
2148            CommitState::CommittingMvcc { .. }
2149        ) {
2150            let CommitState::CommittingMvcc { state_machine } = &mut program_state.commit_state
2151            else {
2152                unreachable!()
2153            };
2154            match self.step_end_mvcc_txn(state_machine, mv_store)? {
2155                IOResult::Done(_) => {
2156                    assert!(state_machine.is_finalized());
2157                    conn.set_mv_tx(None);
2158                    conn.set_tx_state(TransactionState::None);
2159                    pager.end_read_tx();
2160                    program_state.commit_state = CommitState::Ready;
2161                    // Fall through to attached phase
2162                }
2163                IOResult::IO(io) => return Ok(IOResult::IO(io)),
2164            }
2165        }
2166
2167        // Phase 2: Commit MVCC transactions on attached databases
2168        // Resume an in-progress attached MVCC commit
2169        if matches!(
2170            program_state.commit_state,
2171            CommitState::CommittingAttachedMvcc { .. }
2172        ) {
2173            let (step_result, db_id) = {
2174                let CommitState::CommittingAttachedMvcc {
2175                    state_machine,
2176                    db_id,
2177                    mv_store: ref attached_mv,
2178                } = &mut program_state.commit_state
2179                else {
2180                    unreachable!()
2181                };
2182                (state_machine.step(attached_mv)?, *db_id)
2183            };
2184            match step_result {
2185                IOResult::Done(_) => {
2186                    let attached_pager = conn
2187                        .get_pager_from_database_index(&db_id)
2188                        .expect("attached MVCC transaction should always have a pager");
2189                    conn.publish_database_schema(db_id);
2190                    conn.set_mv_tx_for_db(db_id, None);
2191                    attached_pager.end_read_tx();
2192                    // Fall through to look for more
2193                }
2194                IOResult::IO(io) => return Ok(IOResult::IO(io)),
2195            }
2196        }
2197
2198        // Start/continue committing remaining attached MVCC transactions
2199        loop {
2200            let Some((db_id, tx_id, _mode)) = conn.next_attached_mv_tx() else {
2201                break;
2202            };
2203            let Some(attached_mv_store) = conn.mv_store_for_db(db_id) else {
2204                conn.set_mv_tx_for_db(db_id, None);
2205                continue;
2206            };
2207            let mut state_machine = match attached_mv_store.commit_tx(tx_id, &conn, db_id) {
2208                Ok(sm) => sm,
2209                Err(e) => {
2210                    tracing::error!(
2211                        db_id,
2212                        "attached DB commit failed after main DB already committed; \
2213                         cross-database state is inconsistent: {e}"
2214                    );
2215                    // Rollback remaining uncommitted attached MVCC transactions
2216                    // so they don't block checkpointing until connection close.
2217                    conn.rollback_attached_mvcc_txs(true);
2218                    return Err(e);
2219                }
2220            };
2221            match state_machine.step(&attached_mv_store)? {
2222                IOResult::Done(_) => {
2223                    let attached_pager = conn
2224                        .get_pager_from_database_index(&db_id)
2225                        .expect("attached MVCC transaction should always have a pager");
2226                    conn.publish_database_schema(db_id);
2227                    conn.set_mv_tx_for_db(db_id, None);
2228                    attached_pager.end_read_tx();
2229                    continue;
2230                }
2231                IOResult::IO(io) => {
2232                    program_state.commit_state = CommitState::CommittingAttachedMvcc {
2233                        state_machine,
2234                        db_id,
2235                        mv_store: attached_mv_store,
2236                    };
2237                    return Ok(IOResult::IO(io));
2238                }
2239            }
2240        }
2241
2242        // Phase 3: Commit WAL transactions on attached databases that don't use MVCC.
2243        // When the main DB uses MVCC, we route through commit_txn_mvcc, but attached
2244        // DBs may use WAL mode and need their dirty pages committed via the WAL path.
2245        if matches!(program_state.commit_state, CommitState::CommittingAttached) {
2246            // Re-entry after IO yield from attached WAL pager commit.
2247            match self.end_attached_write_txns(&conn, rollback)? {
2248                IOResult::Done(_) => {
2249                    program_state.commit_state = CommitState::Ready;
2250                    self.end_attached_read_txns(&conn);
2251                    return Ok(IOResult::Done(()));
2252                }
2253                IOResult::IO(io) => return Ok(IOResult::IO(io)),
2254            }
2255        }
2256
2257        match self.end_attached_write_txns(&conn, rollback)? {
2258            IOResult::Done(_) => {}
2259            IOResult::IO(io) => {
2260                program_state.commit_state = CommitState::CommittingAttached;
2261                return Ok(IOResult::IO(io));
2262            }
2263        }
2264        self.end_attached_read_txns(&conn);
2265
2266        program_state.commit_state = CommitState::Ready;
2267        Ok(IOResult::Done(()))
2268    }
2269
2270    #[instrument(skip(self, pager, connection, program_state), level = Level::DEBUG)]
2271    fn step_end_write_txn(
2272        &self,
2273        pager: &Arc<Pager>,
2274        connection: &Connection,
2275        program_state: &mut ProgramState,
2276        rollback: bool,
2277    ) -> Result<IOResult<()>> {
2278        let commit_state = &mut program_state.commit_state;
2279        if matches!(commit_state, CommitState::CommittingAttached) {
2280            // Resume committing attached pagers after IO yield.
2281            match self.end_attached_write_txns(connection, rollback)? {
2282                IOResult::Done(_) => {
2283                    *commit_state = CommitState::Ready;
2284                }
2285                IOResult::IO(io) => {
2286                    return Ok(IOResult::IO(io));
2287                }
2288            }
2289            // Release read locks on attached pagers that only had read transactions
2290            // (end_attached_write_txns only handles pagers with write locks).
2291            self.end_attached_read_txns(connection);
2292            return Ok(IOResult::Done(()));
2293        }
2294        let txn_finish_result = if !rollback {
2295            pager.commit_tx(connection, true)
2296        } else {
2297            pager.rollback_tx(connection);
2298            Ok(IOResult::Done(()))
2299        };
2300        tracing::debug!("txn_finish_result: {:?}", txn_finish_result);
2301        match txn_finish_result? {
2302            IOResult::Done(_) => {
2303                // Main pager commit done, now commit attached database pagers
2304                match self.end_attached_write_txns(connection, rollback)? {
2305                    IOResult::Done(_) => {
2306                        *commit_state = CommitState::Ready;
2307                    }
2308                    IOResult::IO(io) => {
2309                        *commit_state = CommitState::CommittingAttached;
2310                        return Ok(IOResult::IO(io));
2311                    }
2312                }
2313            }
2314            IOResult::IO(io) => {
2315                tracing::trace!("Cacheflush IO");
2316                *commit_state = CommitState::Committing;
2317                return Ok(IOResult::IO(io));
2318            }
2319        }
2320        // Release read locks on attached pagers that only had read transactions
2321        // (end_attached_write_txns only handles pagers with write locks).
2322        self.end_attached_read_txns(connection);
2323        Ok(IOResult::Done(()))
2324    }
2325
2326    /// End write transactions on all attached databases that hold write locks.
2327    /// Iterates ALL attached pagers (not just the current program's write_databases)
2328    /// because in explicit transactions, the COMMIT statement's program may differ
2329    /// from the statement that acquired the attached write lock.
2330    /// On IO yield, already-committed pagers are skipped on re-entry via holds_write_lock().
2331    fn end_attached_write_txns(
2332        &self,
2333        connection: &Connection,
2334        rollback: bool,
2335    ) -> Result<IOResult<()>> {
2336        connection.with_all_attached_pagers_with_index(|pagers| {
2337            for (db_id, attached_pager) in pagers {
2338                let db_id = *db_id;
2339                // MVCC-enabled attached DBs are committed in commit_txn_mvcc phase 2
2340                if connection.mv_store_for_db(db_id).is_some() {
2341                    continue;
2342                }
2343                if !attached_pager.holds_write_lock() {
2344                    continue;
2345                }
2346                if !rollback {
2347                    // Commit dirty pages to WAL, then end write+read transactions.
2348                    // We disable auto-checkpoint and avoid pager.commit_tx() since
2349                    // the checkpoint logic can leave read locks held.
2350                    match attached_pager.commit_wal(
2351                        WalAutoActions::empty(),
2352                        SyncMode::Normal,
2353                        false,
2354                    ) {
2355                        Ok(IOResult::Done(_)) => {}
2356                        Ok(IOResult::IO(io)) => {
2357                            // IO pending — return so the caller can yield and re-enter.
2358                            // commit_wal tracks its own internal state, so calling
2359                            // it again on re-entry will resume correctly.
2360                            return Ok(IOResult::IO(io));
2361                        }
2362                        Err(e) => return Err(e),
2363                    }
2364                    // WAL commit succeeded — publish the connection-local schema
2365                    // changes to the shared Database so other connections can see them.
2366                    connection.publish_database_schema(db_id);
2367                    attached_pager.end_write_tx();
2368                    attached_pager.end_read_tx();
2369                    attached_pager.commit_wal_end();
2370                } else {
2371                    // Discard any local schema changes on rollback
2372                    connection.database_schemas().write().remove(&db_id);
2373                    attached_pager.rollback_attached();
2374                }
2375            }
2376            Ok(IOResult::Done(()))
2377        })
2378    }
2379
2380    /// End read transactions on all attached databases that had transactions started.
2381    fn end_attached_read_txns(&self, connection: &Connection) {
2382        connection.with_all_attached_pagers_with_index(|pagers| {
2383            pagers.iter().for_each(|(db_id, attached_pager)| {
2384                if connection.mv_store_for_db(*db_id).is_some() {
2385                    // MVCC-enabled attached DBs don't use WAL read transactions, so skip.
2386                    return;
2387                }
2388                if attached_pager.holds_write_lock() {
2389                    // Attached pager has a write lock, so its read transaction was ended by end_attached_write_txns: skip.
2390                    return;
2391                }
2392                if attached_pager.holds_read_lock() {
2393                    attached_pager.end_read_tx();
2394                }
2395            });
2396        })
2397    }
2398
2399    #[instrument(skip(self, commit_state, mv_store), level = Level::DEBUG)]
2400    fn step_end_mvcc_txn(
2401        &self,
2402        commit_state: &mut StateMachine<Box<MvccCommitStateMachine>>,
2403        mv_store: &Arc<MvStore>,
2404    ) -> Result<IOResult<()>> {
2405        commit_state.step(mv_store)
2406    }
2407
2408    /// Aborts the program due to various conditions (explicit error, interrupt or reset of unfinished statement) by rolling back the transaction
2409    /// This method is no-op if program was already finished (either aborted or executed to completion)
2410    /// Returns an error if cleanup operations (savepoint rollback/release) fail.
2411    pub fn abort(
2412        &self,
2413        pager: &Arc<Pager>,
2414        err: Option<&LimboError>,
2415        state: &mut ProgramState,
2416    ) -> Result<()> {
2417        fn capture_abort_error(
2418            abort_error: &mut Option<LimboError>,
2419            err: LimboError,
2420            context: &str,
2421        ) {
2422            tracing::error!("{context}: {err}");
2423            if abort_error.is_none() {
2424                *abort_error = Some(err);
2425            }
2426        }
2427
2428        let mut abort_error: Option<LimboError> = None;
2429        // MVCC auto-checkpoint is owned by commit_state, not by normal_step().
2430        // If its yielded I/O fails, normal_step sees the error before
2431        // CommitStateMachine::Checkpoint gets another step, so the checkpoint
2432        // state machine cannot run its own error cleanup. abort() is the first
2433        // statement cleanup path that still owns that commit_state.
2434        state.commit_state.cleanup_mvcc_checkpoint_state();
2435        // If a CommitStateMachine was non-terminal when the program was
2436        // aborted — Statement dropped mid-IO yield, or `?` propagated a Busy
2437        // out of BeginCommitLogicalLog / SyncLogicalLog — release the locks
2438        // it acquired (`pager_commit_lock`, `exclusive_tx`) and roll back the
2439        // orphan tx. Without this the tx stays in `Preparing`, the next op on
2440        // this connection trips a `turso_assert_eq!(Active)`, and any other
2441        // writer parks forever on the leaked `pager_commit_lock`. The
2442        // following err-match's no-rollback arms (Busy / TxError / etc.)
2443        // would otherwise skip this cleanup.
2444        state
2445            .commit_state
2446            .cleanup_abandoned_mvcc_commit(&self.connection);
2447
2448        // ParseSchema owns a nested helper statement on this connection and
2449        // stores `auto_commit=false` for its duration. If the program aborts
2450        // while that state is live (error mid-schema-row), release it here:
2451        // restore the saved auto_commit and drop the inner statement so its
2452        // nested guard is released BEFORE the `is_nested_stmt()` check below.
2453        // Otherwise this top-level statement misclassifies itself as nested,
2454        // skips transaction rollback, and leaks the DDL's exclusive MVCC tx
2455        // (and the cleared auto_commit) into subsequent statements — which
2456        // then appear to succeed without ever committing.
2457        if let Some(inner) = state.active_op_state.take_parse_schema_if_active() {
2458            self.connection
2459                .auto_commit
2460                .store(inner.previous_auto_commit(), Ordering::SeqCst);
2461            drop(inner);
2462        }
2463
2464        // VACUUM (and VACUUM INTO) state can own internal helper statements whose drop path
2465        // releases nested guards. Clean it before checking whether this program
2466        // is itself nested; otherwise abort could skip top-level cleanup.
2467        if let Err(err) = execute::cleanup_vacuum_state(&self.connection, state) {
2468            capture_abort_error(
2469                &mut abort_error,
2470                err,
2471                "Failed to clean up VACUUM state during abort",
2472            );
2473        }
2474
2475        // Only end trigger execution if the subprogram was actually running.
2476        // Cached (pooled) statements may be dropped after their trigger execution
2477        // was already ended by op_program; calling end again would pop the wrong
2478        // entry from the executing_triggers stack.
2479        if self.is_trigger_subprogram() && state.execution_state.is_running() {
2480            self.connection.end_trigger_execution();
2481        }
2482        // Roll back any in-flight autonomous sequence inner-tx and restore
2483        // the connection's `mv_tx` slot to the saved outer tx BEFORE the
2484        // statement-savepoint rollback path runs below. Without this, the
2485        // savepoint rollback below reads `connection.get_mv_tx_id()` and
2486        // gets the now-dead inner tx id — there is no savepoint on the
2487        // inner tx, so the outer tx's statement-level changes (rows
2488        // inserted before the failing nextval) never get rolled back.
2489        // Roll back the statement-level MVCC savepoint on the OUTER tx
2490        // before any downstream cleanup re-targets `connection.mv_tx`.
2491        // The savepoint was opened by `begin_statement` against the outer
2492        // tx; if a `SequenceBeginInnerTx` swap happened mid-statement the
2493        // connection's `mv_tx` slot now points at the (failed) inner tx,
2494        // and `end_statement`'s `rollback_first_savepoint` would walk the
2495        // wrong tx — leaving the outer tx's pre-error writes durable on
2496        // commit. Use `saved_outer` from the pending inner-tx record to
2497        // pick the right tx id, run the savepoint rollback explicitly,
2498        // and let the existing `Statement::cleanup_orphaned_seq_inner_tx`
2499        // (called from `Statement::step` after this abort returns)
2500        // perform the inner-tx rollback + mv_tx restoration.
2501        if err.is_some() && !pager.is_checkpointing() {
2502            if let Some(pending) = state.sequence_inner_tx_pending.as_ref() {
2503                if let Some((outer_tx_id, _)) = pending.saved_outer {
2504                    if let Some(mv_store) = self.connection.mv_store_for_db(pending.db) {
2505                        if let Err(e) = mv_store.rollback_first_savepoint(outer_tx_id) {
2506                            tracing::error!(
2507                                "Failed to rollback outer-tx savepoint after sequence \
2508                                 inner-tx aborted: {e}"
2509                            );
2510                        }
2511                    }
2512                }
2513            }
2514        }
2515        // Errors from nested statements are handled by the parent statement.
2516        if !self.connection.is_nested_stmt() && !self.is_trigger_subprogram() {
2517            let unfinished_statement_reset_or_drop =
2518                err.is_none() && state.execution_state.is_running();
2519            let inside_explicit_transaction = !self.connection.get_auto_commit();
2520            let unfinished_writer = state.is_active_write;
2521            let can_rollback_just_this_statement =
2522                state.auto_txn_cleanup == TxnCleanup::RollbackSavepoint;
2523
2524            let poison_tx = unfinished_statement_reset_or_drop
2525                && inside_explicit_transaction
2526                && unfinished_writer
2527                && !can_rollback_just_this_statement;
2528            if poison_tx {
2529                // Example: BEGIN; UPDATE rows SET ... writes one row, then
2530                // returns IO before reaching Done. If the caller drops that
2531                // statement, we cannot pretend COMMIT is still safe: there is
2532                // no statement savepoint to undo only the partial UPDATE.
2533                self.connection.mark_tx_poisoned();
2534            }
2535
2536            let can_autocommit_now = state.can_autocommit_now(&self.connection);
2537            let is_mvcc = self.connection.mv_store().is_some();
2538            let changed_shared_mvcc_auto_txn = !can_autocommit_now
2539                && state.auto_txn_cleanup == TxnCleanup::RollbackTxn
2540                && state.n_change.load(Ordering::SeqCst) > 0;
2541            if changed_shared_mvcc_auto_txn {
2542                turso_assert!(
2543                    is_mvcc,
2544                    "shared autocommit transaction needed full rollback outside MVCC"
2545                );
2546                // A writer changed rows in an MVCC autocommit transaction, but
2547                // a sibling reader is still holding that transaction open. The
2548                // writer had no statement savepoint, so the only safe cleanup
2549                // is rolling back the whole MVCC transaction.
2550            }
2551            let must_rollback_tx_if_needed = can_autocommit_now || changed_shared_mvcc_auto_txn;
2552            if err.is_some() && !pager.is_checkpointing() {
2553                // For ON CONFLICT FAIL, do NOT rollback the statement savepoint —
2554                // changes made before the error should persist.
2555                // For all other resolve types (ABORT, ROLLBACK, etc.), rollback the statement.
2556                let is_fail_constraint = (matches!(err, Some(LimboError::Constraint(_)))
2557                    && self.resolve_type == ResolveType::Fail)
2558                    || matches!(err, Some(LimboError::Raise(ResolveType::Fail, _)));
2559                if !is_fail_constraint {
2560                    if let Err(end_stmt_err) = state.end_statement(
2561                        &self.connection,
2562                        pager,
2563                        EndStatement::RollbackSavepoint,
2564                    ) {
2565                        capture_abort_error(
2566                            &mut abort_error,
2567                            end_stmt_err,
2568                            "Failed to rollback statement savepoint during abort",
2569                        );
2570                    }
2571                }
2572            }
2573            match err {
2574                // Transaction errors, e.g. trying to start a nested transaction, do not cause a rollback.
2575                Some(LimboError::TxError(_)) => {}
2576                // Table locked errors, e.g. trying to checkpoint in an interactive transaction, do not cause a rollback.
2577                Some(LimboError::TableLocked) => {}
2578                // Busy errors do not cause a rollback.
2579                Some(LimboError::Busy) => {}
2580                // Same-connection "SQL statements in progress" rejections do
2581                // not cause a rollback either: the rejected operation was
2582                // refused before it touched any transaction or savepoint
2583                // state, and the in-progress statement it collided with must
2584                // keep running unharmed.
2585                Some(LimboError::StatementsInProgress(_)) => {}
2586                // BusySnapshot errors do not cause a rollback either - user must rollback explicitly.
2587                // BusySnapshot is distinct from Busy in that a busy_timeout or handler should not be
2588                // used because it will not help - the snapshot is permanently stale and rollback is
2589                // the only way out for this poor transaction.
2590                Some(LimboError::BusySnapshot) => {}
2591                // Schema updated errors do not cause a rollback; the statement will be reprepared and retried,
2592                // and the caller is expected to handle transaction cleanup explicitly if needed.
2593                Some(LimboError::SchemaUpdated) => {}
2594                Some(LimboError::WriteWriteConflict | LimboError::SchemaConflict) => {
2595                    // These MVCC errors mean the current transaction cannot
2596                    // commit. Roll it back even if this statement opened a
2597                    // statement savepoint, as DDL does.
2598                    self.rollback_current_txn(pager);
2599                    self.connection.set_changes(0);
2600                }
2601                // Foreign key constraint errors: ON CONFLICT does NOT apply to FK violations.
2602                // FK errors always behave like ABORT: rollback statement,
2603                // rollback transaction in autocommit mode.
2604                Some(LimboError::ForeignKeyConstraint(_)) => {
2605                    if must_rollback_tx_if_needed {
2606                        self.rollback_current_txn(pager);
2607                    }
2608                    self.connection.set_changes(0);
2609                }
2610                // Constraint and RAISE errors: behavior depends on the effective resolve type.
2611                // For normal constraints, the resolve type comes from the statement (ON CONFLICT).
2612                // For RAISE errors, the resolve type is embedded in the error variant itself.
2613                // - ROLLBACK: rollback the entire transaction regardless of autocommit mode
2614                // - FAIL: don't rollback anything - changes persist, transaction stays active
2615                // - ABORT (default): rollback statement, rollback txn if autocommit
2616                Some(LimboError::Constraint(_)) | Some(LimboError::Raise(_, _)) => {
2617                    let effective_resolve = match err {
2618                        Some(LimboError::Raise(rt, _)) => *rt,
2619                        _ => self.resolve_type,
2620                    };
2621                    match effective_resolve {
2622                        ResolveType::Rollback => {
2623                            self.rollback_current_txn(pager);
2624                            // All deferred FK violations are undone by the full rollback.
2625                            self.connection.clear_deferred_foreign_key_violations();
2626                        }
2627                        ResolveType::Fail => {
2628                            // FAIL: Don't rollback the transaction.
2629                            // Changes made before the error persist.
2630                            if let Err(end_stmt_err) = state.end_statement(
2631                                &self.connection,
2632                                pager,
2633                                EndStatement::ReleaseSavepoint,
2634                            ) {
2635                                capture_abort_error(
2636                                    &mut abort_error,
2637                                    end_stmt_err,
2638                                    "Failed to release statement savepoint during abort",
2639                                );
2640                            }
2641                            if can_autocommit_now {
2642                                // Autocommit FAIL: commit partial changes.
2643                                // This matches halt()'s FAIL+autocommit path.
2644                                let mv_store = self.connection.mv_store();
2645                                if let Err(e) = execute::vtab_commit_all(&self.connection) {
2646                                    capture_abort_error(
2647                                        &mut abort_error,
2648                                        e,
2649                                        "vtab_commit_all failed during FAIL abort",
2650                                    );
2651                                }
2652                                if let Err(e) = execute::index_method_pre_commit_all(state, pager) {
2653                                    capture_abort_error(
2654                                        &mut abort_error,
2655                                        e,
2656                                        "index_method_pre_commit_all failed during FAIL abort",
2657                                    );
2658                                }
2659                                loop {
2660                                    match self.commit_txn(
2661                                        pager.clone(),
2662                                        state,
2663                                        mv_store.as_ref(),
2664                                        false,
2665                                    ) {
2666                                        Ok(IOResult::Done(_)) => break,
2667                                        Ok(IOResult::IO(io)) => {
2668                                            if let Err(e) = io.wait(pager.io.as_ref()) {
2669                                                capture_abort_error(
2670                                                    &mut abort_error,
2671                                                    e,
2672                                                    "IO error during FAIL commit in abort",
2673                                                );
2674                                                break;
2675                                            }
2676                                        }
2677                                        Err(e) => {
2678                                            capture_abort_error(
2679                                                &mut abort_error,
2680                                                e,
2681                                                "commit_txn failed during FAIL abort",
2682                                            );
2683                                            break;
2684                                        }
2685                                    }
2686                                }
2687                            }
2688                        }
2689                        _ => {
2690                            if must_rollback_tx_if_needed {
2691                                self.rollback_current_txn(pager);
2692                            }
2693                        }
2694                    }
2695                    let last_change = match effective_resolve {
2696                        ResolveType::Fail => state.n_change.load(Ordering::SeqCst),
2697                        _ => 0,
2698                    };
2699                    self.connection.set_changes(last_change);
2700                }
2701                Some(LimboError::RaiseIgnore) => {
2702                    tracing::error!(
2703                        "BUG: RaiseIgnore reached abort() - should be caught by op_program"
2704                    );
2705                    debug_assert!(
2706                        false,
2707                        "RaiseIgnore should be caught by op_program, not reach abort"
2708                    );
2709                }
2710                _ => match state.auto_txn_cleanup {
2711                    TxnCleanup::RollbackTxn => {
2712                        if must_rollback_tx_if_needed {
2713                            self.rollback_current_txn(pager);
2714                        }
2715                    }
2716                    TxnCleanup::RollbackSavepoint => {
2717                        if can_autocommit_now {
2718                            self.rollback_current_txn(pager);
2719                        } else if err.is_none() && !pager.is_checkpointing() {
2720                            if let Err(end_stmt_err) = state.end_statement(
2721                                &self.connection,
2722                                pager,
2723                                EndStatement::RollbackSavepoint,
2724                            ) {
2725                                capture_abort_error(
2726                                    &mut abort_error,
2727                                    end_stmt_err,
2728                                    "Failed to rollback statement savepoint during abort",
2729                                );
2730                            }
2731                        }
2732                    }
2733                    TxnCleanup::None => {
2734                        if can_autocommit_now
2735                            || (!self.connection.get_auto_commit() && err.is_some())
2736                        {
2737                            self.rollback_current_txn(pager);
2738                        }
2739                    }
2740                },
2741            }
2742        }
2743        if state.uses_subjournal {
2744            pager.stop_use_subjournal();
2745            state.uses_subjournal = false;
2746        }
2747        state.auto_txn_cleanup = TxnCleanup::None;
2748        if let Some(err) = abort_error {
2749            return Err(err);
2750        }
2751        Ok(())
2752    }
2753
2754    fn rollback_current_txn(&self, pager: &Arc<Pager>) {
2755        self.connection.rollback_current_txn_state(pager, true);
2756    }
2757
2758    pub fn is_trigger_subprogram(&self) -> bool {
2759        self.trigger.is_some() || self.is_subprogram
2760    }
2761}
2762
2763impl Deref for Program {
2764    type Target = PreparedProgram;
2765
2766    fn deref(&self) -> &PreparedProgram {
2767        &self.prepared
2768    }
2769}
2770
2771pub(crate) fn make_record(
2772    registers: &[Register],
2773    start_reg: &usize,
2774    count: &usize,
2775) -> Result<ImmutableRecord> {
2776    let regs = &registers[*start_reg..*start_reg + *count];
2777    ImmutableRecord::from_registers(regs, regs.len())
2778}
2779
2780/// Split a register slice into an immutable ref and a mutable ref at two distinct indices.
2781pub(crate) fn split_registers(
2782    registers: &mut [Register],
2783    src: usize,
2784    dst: usize,
2785) -> (&Register, &mut Register) {
2786    debug_assert_ne!(src, dst, "split_registers: src and dst must differ");
2787    if src < dst {
2788        let (left, right) = registers.split_at_mut(dst);
2789        (&left[src], &mut right[0])
2790    } else {
2791        let (left, right) = registers.split_at_mut(src);
2792        (&right[0], &mut left[dst])
2793    }
2794}
2795
2796pub fn registers_to_ref_values<'a>(
2797    registers: &'a [Register],
2798) -> impl ExactSizeIterator<Item = ValueRef<'a>> {
2799    registers.iter().map(|reg| reg.get_value().as_ref())
2800}
2801
2802#[instrument(skip(program), level = Level::DEBUG)]
2803fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) {
2804    tracing::trace!(
2805        "\n{}",
2806        explain::insn_to_str(
2807            program,
2808            addr,
2809            insn,
2810            String::new(),
2811            program
2812                .comments
2813                .iter()
2814                .find(|(offset, _)| *offset == addr)
2815                .map(|(_, comment)| comment)
2816                .copied()
2817        )
2818    );
2819}
2820
2821pub trait FromValueRow<'a> {
2822    fn from_value(value: &'a Value) -> Result<Self>
2823    where
2824        Self: Sized + 'a;
2825}
2826
2827impl<'a> FromValueRow<'a> for i64 {
2828    fn from_value(value: &'a Value) -> Result<Self> {
2829        match value {
2830            Value::Numeric(Numeric::Integer(i)) => Ok(*i),
2831            _ => Err(LimboError::ConversionError("Expected integer value".into())),
2832        }
2833    }
2834}
2835
2836impl<'a> FromValueRow<'a> for f64 {
2837    fn from_value(value: &'a Value) -> Result<Self> {
2838        match value {
2839            Value::Numeric(Numeric::Float(f)) => Ok(f64::from(*f)),
2840            _ => Err(LimboError::ConversionError("Expected integer value".into())),
2841        }
2842    }
2843}
2844
2845impl<'a> FromValueRow<'a> for String {
2846    fn from_value(value: &'a Value) -> Result<Self> {
2847        match value {
2848            Value::Text(s) => Ok(s.as_str().to_string()),
2849            _ => Err(LimboError::ConversionError("Expected text value".into())),
2850        }
2851    }
2852}
2853
2854impl<'a> FromValueRow<'a> for &'a str {
2855    fn from_value(value: &'a Value) -> Result<Self> {
2856        match value {
2857            Value::Text(s) => Ok(s.as_str()),
2858            _ => Err(LimboError::ConversionError("Expected text value".into())),
2859        }
2860    }
2861}
2862
2863impl<'a> FromValueRow<'a> for &'a Value {
2864    fn from_value(value: &'a Value) -> Result<Self> {
2865        Ok(value)
2866    }
2867}
2868
2869impl Row {
2870    pub fn get<'a, T: FromValueRow<'a> + 'a>(&'a self, idx: usize) -> Result<T> {
2871        let value = unsafe {
2872            self.values
2873                .add(idx)
2874                .as_ref()
2875                .expect("row value pointer should be valid")
2876        };
2877        let value = match value {
2878            Register::Value(value) => value,
2879            _ => unreachable!("a row should be formed of values only"),
2880        };
2881        T::from_value(value)
2882    }
2883
2884    pub fn get_value(&self, idx: usize) -> &Value {
2885        let value = unsafe {
2886            self.values
2887                .add(idx)
2888                .as_ref()
2889                .expect("row value pointer should be valid")
2890        };
2891        match value {
2892            Register::Value(value) => value,
2893            _ => unreachable!("a row should be formed of values only"),
2894        }
2895    }
2896
2897    pub fn get_values(&self) -> impl Iterator<Item = &Value> {
2898        let values = unsafe { std::slice::from_raw_parts(self.values, self.count) };
2899        // This should be ownedvalues
2900        // TODO: add check for this
2901        values.iter().map(|v| v.get_value())
2902    }
2903
2904    pub fn len(&self) -> usize {
2905        self.count
2906    }
2907
2908    pub fn is_empty(&self) -> bool {
2909        self.count == 0
2910    }
2911}
2912
2913/// Extension trait for `ValueIterator` that allows writing directly to a `Register`
2914/// without allocating intermediate `ValueRef` values.
2915pub trait ValueIteratorExt {
2916    /// Skips `n` elements and writes the value directly to the register.
2917    /// Returns `Some(Ok(()))` on success, `Some(Err(...))` on parse error,
2918    /// or `None` if there are fewer than `n+1` elements.
2919    fn nth_into_register(&mut self, n: usize, dest: &mut Register) -> Option<Result<()>>;
2920}
2921
2922impl<'a> ValueIteratorExt for crate::types::ValueIterator<'a> {
2923    #[inline(always)]
2924    fn nth_into_register(&mut self, n: usize, dest: &mut Register) -> Option<Result<()>> {
2925        use crate::storage::sqlite3_ondisk::read_varint;
2926        use crate::types::{get_serial_type_size, Extendable, Text};
2927
2928        let mut header = self.header_section_ref();
2929        let mut data = self.data_section_ref();
2930
2931        // Skip n elements
2932        let mut data_sum = 0;
2933        for _ in 0..n {
2934            if header.is_empty() {
2935                return None;
2936            }
2937
2938            let (serial_type, bytes_read) = match read_varint(header) {
2939                Ok(v) => v,
2940                Err(e) => return Some(Err(e)),
2941            };
2942            header = &header[bytes_read..];
2943
2944            data_sum += match get_serial_type_size(serial_type) {
2945                Ok(size) => size,
2946                Err(e) => return Some(Err(e)),
2947            };
2948        }
2949
2950        if data_sum > data.len() {
2951            return Some(Err(LimboError::Corrupt(
2952                "Data section too small for indicated serial type size".into(),
2953            )));
2954        }
2955        data = &data[data_sum..];
2956
2957        // Read the serial type for the target element
2958        if header.is_empty() {
2959            return None;
2960        }
2961
2962        let (serial_type, bytes_read) = match read_varint(header) {
2963            Ok(v) => v,
2964            Err(e) => return Some(Err(e)),
2965        };
2966
2967        // Update iterator state
2968        self.set_header_section(&header[bytes_read..]);
2969
2970        // Decode directly into register based on serial type
2971        match serial_type {
2972            // NULL
2973            0 => {
2974                self.set_data_section(data);
2975                dest.set_null();
2976            }
2977            // I8
2978            1 => {
2979                if unlikely(data.is_empty()) {
2980                    return Some(Err(LimboError::Corrupt("Invalid 1-byte int".into())));
2981                }
2982                self.set_data_section(&data[1..]);
2983                dest.set_int(data[0] as i8 as i64);
2984            }
2985            // I16
2986            2 => {
2987                if unlikely(data.len() < 2) {
2988                    return Some(Err(LimboError::Corrupt("Invalid 2-byte int".into())));
2989                }
2990                self.set_data_section(&data[2..]);
2991                dest.set_int(i16::from_be_bytes([data[0], data[1]]) as i64);
2992            }
2993            // I24
2994            3 => {
2995                if unlikely(data.len() < 3) {
2996                    return Some(Err(LimboError::Corrupt("Invalid 3-byte int".into())));
2997                }
2998                self.set_data_section(&data[3..]);
2999                let sign_extension = if data[0] <= 0x7F { 0 } else { 0xFF };
3000                dest.set_int(
3001                    i32::from_be_bytes([sign_extension, data[0], data[1], data[2]]) as i64,
3002                );
3003            }
3004            // I32
3005            4 => {
3006                if unlikely(data.len() < 4) {
3007                    return Some(Err(LimboError::Corrupt("Invalid 4-byte int".into())));
3008                }
3009                self.set_data_section(&data[4..]);
3010                dest.set_int(i32::from_be_bytes([data[0], data[1], data[2], data[3]]) as i64);
3011            }
3012            // I48
3013            5 => {
3014                if unlikely(data.len() < 6) {
3015                    return Some(Err(LimboError::Corrupt("Invalid 6-byte int".into())));
3016                }
3017                self.set_data_section(&data[6..]);
3018                let sign_extension = if data[0] <= 0x7F { 0 } else { 0xFF };
3019                dest.set_int(i64::from_be_bytes([
3020                    sign_extension,
3021                    sign_extension,
3022                    data[0],
3023                    data[1],
3024                    data[2],
3025                    data[3],
3026                    data[4],
3027                    data[5],
3028                ]));
3029            }
3030            // I64
3031            6 => {
3032                if unlikely(data.len() < 8) {
3033                    return Some(Err(LimboError::Corrupt("Invalid 8-byte int".into())));
3034                }
3035                self.set_data_section(&data[8..]);
3036                dest.set_int(i64::from_be_bytes([
3037                    data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
3038                ]));
3039            }
3040            // F64
3041            7 => {
3042                if unlikely(data.len() < 8) {
3043                    return Some(Err(LimboError::Corrupt("Invalid 8-byte float".into())));
3044                }
3045                self.set_data_section(&data[8..]);
3046                let val = f64::from_be_bytes([
3047                    data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
3048                ]);
3049                if let Some(nn) = NonNan::new(val) {
3050                    dest.set_float(nn);
3051                } else {
3052                    dest.set_null();
3053                }
3054            }
3055            // CONST_INT0
3056            8 => {
3057                self.set_data_section(data);
3058                dest.set_int(0);
3059            }
3060            // CONST_INT1
3061            9 => {
3062                self.set_data_section(data);
3063                dest.set_int(1);
3064            }
3065            // Reserved
3066            10 | 11 => {
3067                mark_unlikely();
3068                return Some(Err(LimboError::Corrupt(format!(
3069                    "Reserved serial type: {serial_type}"
3070                ))));
3071            }
3072            // BLOB (n >= 12 && n & 1 == 0)
3073            n if n >= 12 && n & 1 == 0 => {
3074                let content_size = ((n - 12) / 2) as usize;
3075                if unlikely(data.len() < content_size) {
3076                    return Some(Err(LimboError::Corrupt("Invalid Blob value".into())));
3077                }
3078                self.set_data_section(&data[content_size..]);
3079                let blob_data = &data[..content_size];
3080                match dest {
3081                    Register::Value(Value::Blob(existing_blob)) => {
3082                        if let Err(err) = existing_blob.do_extend(&blob_data) {
3083                            return Some(Err(err));
3084                        }
3085                    }
3086                    _ => {
3087                        if let Err(err) = dest.set_blob(blob_data.to_vec()) {
3088                            return Some(Err(err));
3089                        }
3090                    }
3091                }
3092            }
3093            // TEXT (n >= 13 && n & 1 == 1)
3094            n if n >= 13 && n & 1 == 1 => {
3095                let content_size = ((n - 13) / 2) as usize;
3096                if unlikely(data.len() < content_size) {
3097                    return Some(Err(LimboError::Corrupt("Invalid Text value".into())));
3098                }
3099                self.set_data_section(&data[content_size..]);
3100                let text_data = &data[..content_size];
3101                // SAFETY: TEXT serial type contains valid UTF-8
3102                let text_str = if cfg!(debug_assertions) {
3103                    match std::str::from_utf8(text_data) {
3104                        Ok(s) => s,
3105                        Err(e) => {
3106                            return Some(Err(LimboError::InternalError(format!(
3107                                "Invalid UTF-8 in TEXT serial type: {e}"
3108                            ))));
3109                        }
3110                    }
3111                } else {
3112                    unsafe { std::str::from_utf8_unchecked(text_data) }
3113                };
3114                match dest {
3115                    Register::Value(Value::Text(existing_text)) => {
3116                        if let Err(err) = existing_text.do_extend(&text_str) {
3117                            return Some(Err(err));
3118                        }
3119                    }
3120                    _ => {
3121                        if let Err(err) = dest.set_text(Text::new(text_str.to_string())) {
3122                            return Some(Err(err));
3123                        }
3124                    }
3125                }
3126            }
3127            _ => {
3128                mark_unlikely();
3129                return Some(Err(LimboError::Corrupt(format!(
3130                    "Invalid serial type: {serial_type}"
3131                ))));
3132            }
3133        }
3134
3135        Some(Ok(()))
3136    }
3137}
3138
3139#[cfg(clt_turso_tests)]
3140mod tests {
3141    use super::*;
3142    use std::panic::{catch_unwind, AssertUnwindSafe};
3143
3144    #[test]
3145    fn active_opcode_helpers_initialize_defaults() {
3146        let mut state = ProgramState::new(1, 0);
3147
3148        assert!(matches!(state.active_op_state.state, ActiveOpState::None));
3149        assert!(matches!(
3150            state.active_op_state.column(),
3151            OpColumnState::Start
3152        ));
3153        state.active_op_state.clear();
3154        assert!(state.active_op_state.parse_schema().is_none());
3155    }
3156
3157    #[test]
3158    fn active_opcode_helpers_reject_mismatched_resumes() {
3159        let mut state = ProgramState::new(1, 0);
3160        *state.active_op_state.column() = OpColumnState::GetColumn;
3161
3162        let panic = catch_unwind(AssertUnwindSafe(|| {
3163            let _ = state.active_op_state.parse_schema();
3164        }));
3165        assert!(panic.is_err(), "mismatched opcode resume should panic");
3166    }
3167
3168    #[test]
3169    fn seek_state_is_independent_from_active_opcode_slot() {
3170        let mut state = ProgramState::new(1, 0);
3171
3172        *state.active_op_state.insert() = OpInsertState {
3173            sub_state: OpInsertSubState::Seek,
3174            old_record: None,
3175            is_noop_update: false,
3176        };
3177        state.seek_state = OpSeekState::MoveLast;
3178
3179        assert!(matches!(
3180            state.active_op_state.insert().sub_state,
3181            OpInsertSubState::Seek
3182        ));
3183        assert!(matches!(state.seek_state, OpSeekState::MoveLast));
3184    }
3185}
3186
3187/// Shuttle tests for validating the `unsafe impl Send + Sync for ProgramState` safety claims.
3188///
3189/// The safety claims are:
3190/// 1. `Row` contains a `*const Register` pointing into `ProgramState.registers`
3191/// 2. Only immutable references (`&Row`) are given out via `result_row.as_ref()`
3192/// 3. `result_row` is invalidated (via `.take()`) at the start of each step iteration
3193///
3194/// These tests verify that the implementation correctly upholds these invariants
3195/// under concurrent access patterns.
3196
3197#[cfg(all(shuttle, clt_turso_tests))]
3198mod shuttle_tests {
3199    use super::*;
3200    use crate::sync::Arc;
3201    use crate::thread;
3202    use crate::types::Value;
3203
3204    /// Creates a minimal ProgramState for testing.
3205    fn create_test_state(num_registers: usize, num_cursors: usize) -> ProgramState {
3206        ProgramState::new(num_registers, num_cursors)
3207    }
3208
3209    /// Test that ProgramState can be safely sent between threads.
3210    /// This validates the `unsafe impl Send for ProgramState` claim.
3211    #[test]
3212    fn shuttle_program_state_send() {
3213        shuttle::check_random(
3214            || {
3215                let mut state = create_test_state(10, 2);
3216
3217                // Write some data to registers
3218                state.registers[0].set_int(42);
3219                state.registers[1].set_text(Text::new("test".to_string()));
3220
3221                // Send state to another thread
3222                let handle = thread::spawn(move || {
3223                    // Verify data is intact after send
3224                    assert!(matches!(
3225                        &state.registers[0],
3226                        Register::Value(Value::Numeric(Numeric::Integer(42)))
3227                    ));
3228                    if let Register::Value(Value::Text(t)) = &state.registers[1] {
3229                        assert_eq!(t.as_str(), "test");
3230                    } else {
3231                        panic!("Expected text value");
3232                    }
3233
3234                    // Modify in new thread
3235                    state.registers[2].set_int(100);
3236                    state
3237                });
3238
3239                let state = handle.join().unwrap();
3240                assert!(matches!(
3241                    &state.registers[2],
3242                    Register::Value(Value::Numeric(Numeric::Integer(100)))
3243                ));
3244            },
3245            1000,
3246        );
3247    }
3248
3249    /// Test that ProgramState with a set result_row can be safely sent.
3250    /// The Row contains a raw pointer that must remain valid after the send.
3251    #[test]
3252    fn shuttle_program_state_send_with_row() {
3253        shuttle::check_random(
3254            || {
3255                let mut state = create_test_state(10, 2);
3256
3257                // Set up registers with test data
3258                state.registers[0].set_int(1);
3259                state.registers[1].set_int(2);
3260                state.registers[2].set_int(3);
3261
3262                // Create a result_row pointing to registers
3263                state.result_row = Some(Row {
3264                    values: &state.registers[0] as *const Register,
3265                    count: 3,
3266                });
3267
3268                // Send to another thread - the pointer must remain valid
3269                // because it points to memory owned by state (the registers Vec)
3270                let handle = thread::spawn(move || {
3271                    // The row pointer should still be valid because registers moved with state
3272                    if let Some(row) = &state.result_row {
3273                        assert_eq!(row.len(), 3);
3274                        // Read through the pointer - this validates the pointer is still valid
3275                        let val = row.get::<i64>(0).unwrap();
3276                        assert_eq!(val, 1);
3277                        let val = row.get::<i64>(1).unwrap();
3278                        assert_eq!(val, 2);
3279                        let val = row.get::<i64>(2).unwrap();
3280                        assert_eq!(val, 3);
3281                    } else {
3282                        panic!("Expected result_row to be set");
3283                    }
3284                    state
3285                });
3286
3287                let _ = handle.join().unwrap();
3288            },
3289            1000,
3290        );
3291    }
3292
3293    /// Test concurrent reads of result_row through shared reference.
3294    /// This validates the `unsafe impl Sync for ProgramState` claim for read access.
3295    #[test]
3296    fn shuttle_program_state_sync_concurrent_reads() {
3297        shuttle::check_random(
3298            || {
3299                let mut state = create_test_state(10, 2);
3300
3301                // Set up registers
3302                state.registers[0].set_int(42);
3303                state.registers[1].set_int(43);
3304
3305                // Create result_row
3306                state.result_row = Some(Row {
3307                    values: &state.registers[0] as *const Register,
3308                    count: 2,
3309                });
3310
3311                let state = Arc::new(state);
3312                let state2 = Arc::clone(&state);
3313                let state3 = Arc::clone(&state);
3314
3315                // Multiple threads reading concurrently
3316                let h1 = thread::spawn(move || {
3317                    if let Some(row) = &state.result_row {
3318                        let val = row.get::<i64>(0).unwrap();
3319                        assert_eq!(val, 42);
3320                    }
3321                });
3322
3323                let h2 = thread::spawn(move || {
3324                    if let Some(row) = &state2.result_row {
3325                        let val = row.get::<i64>(1).unwrap();
3326                        assert_eq!(val, 43);
3327                    }
3328                });
3329
3330                let h3 = thread::spawn(move || {
3331                    if let Some(row) = &state3.result_row {
3332                        assert_eq!(row.len(), 2);
3333                    }
3334                });
3335
3336                h1.join().unwrap();
3337                h2.join().unwrap();
3338                h3.join().unwrap();
3339            },
3340            1000,
3341        );
3342    }
3343
3344    /// Test that Row values read through the pointer are consistent.
3345    /// Multiple threads reading the same row values should see the same data.
3346    #[test]
3347    fn shuttle_row_pointer_consistency() {
3348        shuttle::check_random(
3349            || {
3350                let mut state = create_test_state(10, 2);
3351
3352                // Set up registers with distinct values
3353                for i in 0..5 {
3354                    state.registers[i].set_int(i as i64 * 10);
3355                }
3356
3357                state.result_row = Some(Row {
3358                    values: &state.registers[0] as *const Register,
3359                    count: 5,
3360                });
3361
3362                let state = Arc::new(state);
3363                let mut handles = vec![];
3364
3365                for _ in 0..4 {
3366                    let state_clone = Arc::clone(&state);
3367                    let h = thread::spawn(move || {
3368                        if let Some(row) = &state_clone.result_row {
3369                            // All threads should see the same values
3370                            for i in 0..5 {
3371                                let val = row.get::<i64>(i).unwrap();
3372                                assert_eq!(val, i as i64 * 10);
3373                            }
3374                        }
3375                    });
3376                    handles.push(h);
3377                }
3378
3379                for h in handles {
3380                    h.join().unwrap();
3381                }
3382            },
3383            1000,
3384        );
3385    }
3386
3387    /// Test the result_row invalidation pattern.
3388    /// When result_row is taken (invalidated), concurrent reads should not see stale data.
3389    /// This simulates the pattern used in `normal_step()` where `result_row.take()` is called.
3390    #[test]
3391    fn shuttle_result_row_invalidation() {
3392        shuttle::check_random(
3393            || {
3394                let mut state = create_test_state(10, 2);
3395
3396                state.registers[0].set_int(100);
3397                state.result_row = Some(Row {
3398                    values: &state.registers[0] as *const Register,
3399                    count: 1,
3400                });
3401
3402                // Simulate the invalidation pattern from normal_step
3403                // In real code, this requires &mut self, so there's no concurrent access
3404                let taken_row = state.result_row.take();
3405
3406                // After take(), result_row should be None
3407                assert!(state.result_row.is_none());
3408
3409                // The taken row still holds valid data (until dropped)
3410                if let Some(row) = taken_row {
3411                    let val = row.get::<i64>(0).unwrap();
3412                    assert_eq!(val, 100);
3413                }
3414            },
3415            1000,
3416        );
3417    }
3418
3419    /// Test register modification after row invalidation.
3420    /// This validates that modifying registers after take() is safe.
3421    #[test]
3422    fn shuttle_register_modification_after_invalidation() {
3423        shuttle::check_random(
3424            || {
3425                let mut state = create_test_state(10, 2);
3426
3427                state.registers[0].set_int(1);
3428                state.result_row = Some(Row {
3429                    values: &state.registers[0] as *const Register,
3430                    count: 1,
3431                });
3432
3433                // Invalidate row (simulating what normal_step does)
3434                let _ = state.result_row.take();
3435
3436                // Now safe to modify registers
3437                state.registers[0].set_int(999);
3438
3439                // Create new row pointing to modified registers
3440                state.result_row = Some(Row {
3441                    values: &state.registers[0] as *const Register,
3442                    count: 1,
3443                });
3444
3445                // New row should see new value
3446                if let Some(row) = &state.result_row {
3447                    let val = row.get::<i64>(0).unwrap();
3448                    assert_eq!(val, 999);
3449                }
3450            },
3451            1000,
3452        );
3453    }
3454
3455    /// Test sequential send-receive pattern (simulating async task scheduling).
3456    /// ProgramState is moved between threads in a producer-consumer pattern.
3457    #[test]
3458    fn shuttle_sequential_thread_transfer() {
3459        shuttle::check_random(
3460            || {
3461                let mut state = create_test_state(10, 2);
3462                state.registers[0].set_int(0);
3463
3464                // Thread 1: increment
3465                let h1 = thread::spawn(move || {
3466                    if let Register::Value(Value::Numeric(Numeric::Integer(v))) =
3467                        &state.registers[0]
3468                    {
3469                        state.registers[0].set_int(v + 1);
3470                    }
3471                    state
3472                });
3473
3474                let mut state = h1.join().unwrap();
3475
3476                // Thread 2: increment
3477                let h2 = thread::spawn(move || {
3478                    if let Register::Value(Value::Numeric(Numeric::Integer(v))) =
3479                        &state.registers[0]
3480                    {
3481                        state.registers[0].set_int(v + 1);
3482                    }
3483                    state
3484                });
3485
3486                let mut state = h2.join().unwrap();
3487
3488                // Thread 3: increment
3489                let h3 = thread::spawn(move || {
3490                    if let Register::Value(Value::Numeric(Numeric::Integer(v))) =
3491                        &state.registers[0]
3492                    {
3493                        state.registers[0].set_int(v + 1);
3494                    }
3495                    state
3496                });
3497
3498                let state = h3.join().unwrap();
3499
3500                // Final value should be 3
3501                assert!(matches!(
3502                    &state.registers[0],
3503                    Register::Value(Value::Numeric(Numeric::Integer(3)))
3504                ));
3505            },
3506            1000,
3507        );
3508    }
3509
3510    /// Test that ProgramState can be wrapped in Arc for shared ownership.
3511    /// This is the typical pattern for concurrent database operations.
3512    #[test]
3513    fn shuttle_arc_wrapped_state() {
3514        shuttle::check_random(
3515            || {
3516                let mut state = create_test_state(10, 2);
3517
3518                // Initialize with test data
3519                for i in 0..5 {
3520                    state.registers[i].set_int(i as i64);
3521                }
3522
3523                let state = Arc::new(state);
3524                let mut handles = vec![];
3525
3526                // Multiple threads reading registers through Arc
3527                for thread_id in 0u8..4 {
3528                    let state_clone = Arc::clone(&state);
3529                    let h = thread::spawn(move || {
3530                        // Each thread reads all registers
3531                        for i in 0..5 {
3532                            if let Register::Value(Value::Numeric(Numeric::Integer(v))) =
3533                                &state_clone.registers[i]
3534                            {
3535                                assert_eq!(*v, i as i64);
3536                            }
3537                        }
3538                        thread_id
3539                    });
3540                    handles.push(h);
3541                }
3542
3543                for h in handles {
3544                    h.join().unwrap();
3545                }
3546            },
3547            1000,
3548        );
3549    }
3550
3551    /// Test Row::get_values iterator under concurrent access.
3552    #[test]
3553    fn shuttle_row_get_values_concurrent() {
3554        shuttle::check_random(
3555            || {
3556                let mut state = create_test_state(10, 2);
3557
3558                state.registers[0].set_int(10);
3559                state.registers[1].set_int(20);
3560                state.registers[2].set_int(30);
3561
3562                state.result_row = Some(Row {
3563                    values: &state.registers[0] as *const Register,
3564                    count: 3,
3565                });
3566
3567                let state = Arc::new(state);
3568                let state2 = Arc::clone(&state);
3569
3570                let h1 = thread::spawn(move || {
3571                    if let Some(row) = &state.result_row {
3572                        let values: Vec<_> = row.get_values().collect();
3573                        assert_eq!(values.len(), 3);
3574                    }
3575                });
3576
3577                let h2 = thread::spawn(move || {
3578                    if let Some(row) = &state2.result_row {
3579                        let mut sum = 0i64;
3580                        for val in row.get_values() {
3581                            if let Value::Numeric(Numeric::Integer(i)) = val {
3582                                sum += i;
3583                            }
3584                        }
3585                        assert_eq!(sum, 60); // 10 + 20 + 30
3586                    }
3587                });
3588
3589                h1.join().unwrap();
3590                h2.join().unwrap();
3591            },
3592            1000,
3593        );
3594    }
3595
3596    /// Stress test: Many threads reading from shared ProgramState.
3597    #[test]
3598    fn shuttle_stress_concurrent_reads() {
3599        shuttle::check_random(
3600            || {
3601                let mut state = create_test_state(20, 2);
3602
3603                // Fill registers with identifiable data
3604                for i in 0..20 {
3605                    state.registers[i].set_int(i as i64 * 100);
3606                }
3607
3608                state.result_row = Some(Row {
3609                    values: &state.registers[0] as *const Register,
3610                    count: 20,
3611                });
3612
3613                let state = Arc::new(state);
3614                let mut handles = vec![];
3615
3616                for thread_id in 0..6u8 {
3617                    let state_clone = Arc::clone(&state);
3618                    let h = thread::spawn(move || {
3619                        // Each thread reads different parts
3620                        let start = (thread_id as usize * 3) % 20;
3621                        if let Some(row) = &state_clone.result_row {
3622                            for i in 0..3 {
3623                                let idx = (start + i) % row.len();
3624                                let val = row.get::<i64>(idx).unwrap();
3625                                assert_eq!(val, idx as i64 * 100);
3626                            }
3627                        }
3628                        thread_id
3629                    });
3630                    handles.push(h);
3631                }
3632
3633                for h in handles {
3634                    h.join().unwrap();
3635                }
3636            },
3637            1000,
3638        );
3639    }
3640}