Skip to main content

clt_database/
statement.rs

1use std::{
2    borrow::Cow,
3    num::NonZero,
4    ops::Deref,
5    sync::{atomic::Ordering, Arc},
6    task::Waker,
7    time::Duration,
8};
9
10use tracing::{instrument, Level};
11use turso_parser::{
12    ast::{fmt::ToTokens, Cmd},
13    parser::Parser,
14};
15
16use crate::alloc::TursoIteratorExt;
17use crate::{
18    busy::BusyHandlerState,
19    parameters,
20    schema::Trigger,
21    stats::refresh_analyze_stats,
22    translate::{self, display::PlanContext, emitter::TransactionMode, plan::BitSet},
23    turso_assert,
24    vdbe::{
25        self,
26        explain::{EXPLAIN_COLUMNS_TYPE, EXPLAIN_QUERY_PLAN_COLUMNS_TYPE},
27    },
28    LimboError, MvStore, Pager, QueryMode, Result, TransactionState, Value, EXPLAIN_COLUMNS,
29    EXPLAIN_QUERY_PLAN_COLUMNS,
30};
31
32type ProgramExecutionState = vdbe::ProgramExecutionState;
33type Row = vdbe::Row;
34type StepResult = vdbe::StepResult;
35
36/// Classifies how a [`Statement`] participates in connection-level lifecycle
37/// and active-statement accounting.
38///
39/// Use [`StatementOrigin::Root`] for ordinary top-level statements prepared on
40/// behalf of the user. Root statements are the only statements that count
41/// toward `Connection::n_active_root_statements` once execution begins, which
42/// is the SQLite-compatible notion of "another SQL statement in progress" used
43/// by operations like `VACUUM`.
44///
45/// Use [`StatementOrigin::InternalHelper`] when the engine prepares and runs a
46/// separate helper statement on the same connection, for example helper SQL in
47/// schema parsing or CDC setup. This is separately prepared SQL with its own
48/// `prepare`/`step`/`reset`/`drop` lifecycle, but it is owned by a parent root
49/// statement, so it stays nested and does not count as another root statement.
50///
51/// Use [`StatementOrigin::Subprogram`] only for bytecode subprograms that are
52/// already compiled into a parent statement and entered through `OP_Program`,
53/// such as trigger or foreign-key actions. This is not separately prepared SQL;
54/// it is embedded child bytecode execution inside the parent statement.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) enum StatementOrigin {
57    Root,
58    InternalHelper,
59    Subprogram,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum StatementStatusCounter {
64    FullscanStep,
65    Sort,
66    VmStep,
67    Reprepare,
68    RowsRead,
69    RowsWritten,
70}
71
72impl StatementOrigin {
73    pub(crate) const fn needs_nested_guard(self) -> bool {
74        matches!(self, Self::InternalHelper)
75    }
76}
77
78/// Structured type information for a result column.
79///
80/// Returned by [`Statement::get_column_type_info`]. Surfaces the array depth
81/// and custom-type resolution that the SQLite-compat `get_column_decltype`
82/// API does not expose, and also carries the inferred-affinity result for
83/// computed expressions (`SELECT 1+1`, function calls in subqueries, etc.)
84/// — the consumer asks one question, the API decides which path applies.
85///
86/// For a direct table-column reference, `declared_name` is the literal
87/// string the user wrote in CREATE TABLE (`"INTEGER"`, `"cents"`,
88/// `"VARCHAR"`), `array_dimensions` is the bracket depth, and `base_type` /
89/// `kind` carry any CREATE TYPE / CREATE DOMAIN resolution.
90///
91/// For a literal (`SELECT 42`, `SELECT 'x'`, `SELECT 3.14`), `declared_name`
92/// is the primitive that matches the literal's parsed value type
93/// (`"INTEGER"`, `"TEXT"`, `"REAL"`). For a typed expression — CAST, rowid,
94/// or anything else SQLite's affinity rules can pin down — it's the
95/// inferred primitive. In both cases `array_dimensions` is `0`, `base_type`
96/// is `None`, and `kind` is [`ColumnTypeKind::Builtin`]. When neither path
97/// produces a usable primitive (binary arithmetic that SQLite refuses to
98/// propagate through, BLOB literals, NULL literals, function calls without
99/// declared return affinity), `get_column_type_info` returns `Ok(None)`
100/// rather than fabricating a name — callers can fall through to their own
101/// default.
102///
103/// New fields may be added over time; the struct is marked
104/// `#[non_exhaustive]` so consumers must use struct-update or accessor
105/// patterns rather than exhaustive matches.
106#[derive(Debug, Clone, PartialEq, Eq)]
107#[non_exhaustive]
108pub struct ColumnTypeInfo {
109    /// The declared type name as written in CREATE TABLE — e.g. `"INTEGER"`,
110    /// `"VARCHAR"`, or the name of a `CREATE TYPE` / `CREATE DOMAIN` such as
111    /// `"cents"`. This is the same string `get_column_decltype` returns.
112    pub declared_name: String,
113    /// Array dimensionality: `0` for scalar columns, `1` for `INTEGER[]`,
114    /// `2` for `TEXT[][]`, etc.
115    pub array_dimensions: u32,
116    /// For columns whose declared type resolves to a `CREATE TYPE` or
117    /// `CREATE DOMAIN` definition, this is the underlying primitive type name
118    /// (`"INTEGER"`, `"TEXT"`, `"REAL"`, `"BLOB"`, or `"NUMERIC"`). `None`
119    /// when the declared name is a built-in primitive directly.
120    ///
121    /// Use this to distinguish "the user wrote `INTEGER`" (base_type: `None`)
122    /// from "the user wrote `cents`, which happens to be INTEGER underneath"
123    /// (base_type: `Some("INTEGER")`).
124    pub base_type: Option<String>,
125    /// Classification of the declared type. Distinguishes `BUILTIN` (the
126    /// declared name is a primitive) from the four `CREATE TYPE`/`CREATE
127    /// DOMAIN` flavours.
128    ///
129    /// This matters for callers like wire-protocol layers that need to map
130    /// a column to its native type code: a column declared as a `STRUCT`
131    /// type stores a BLOB on disk (`base_type` is `Some("BLOB")`), but the
132    /// caller usually wants to expose it as a composite/JSON type rather
133    /// than raw bytes. The `kind` field carries that distinction directly
134    /// without forcing the caller to re-query the schema.
135    pub kind: ColumnTypeKind,
136}
137
138/// Classification of a result column's declared type.
139///
140/// Returned as part of [`ColumnTypeInfo`]. `#[non_exhaustive]` so that new
141/// kinds (e.g. for future enum or table-row types) can be added without a
142/// breaking change.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum ColumnTypeKind {
146    /// A SQLite-style primitive type: `INTEGER`, `TEXT`, `REAL`, `BLOB`,
147    /// `NUMERIC`, `ANY`. The declared name is itself the primitive.
148    Builtin,
149    /// A user- or built-in custom type defined with
150    /// `CREATE TYPE name BASE primitive ENCODE ... DECODE ...`. Has an
151    /// underlying primitive (see `base_type`) and an encode/decode pipeline.
152    /// Built-in types like `uuid`, `boolean`, `numeric` register through
153    /// this path too.
154    Custom,
155    /// A domain defined with `CREATE DOMAIN name AS base [CHECK ...]`.
156    /// Shares an underlying primitive with its base type but adds CHECK
157    /// constraints; values are otherwise identical to the base.
158    Domain,
159    /// A composite type defined with `CREATE TYPE name AS STRUCT(...)`.
160    /// Values are stored as BLOBs containing the packed record; the
161    /// declared name carries the field schema.
162    Struct,
163    /// A tagged union defined with `CREATE TYPE name AS UNION(...)`.
164    /// Values are stored as BLOBs containing a tag and a payload; the
165    /// declared name carries the variant schema.
166    Union,
167}
168
169/// Recursively infer the result primitive of a non-table-column expression
170/// and return its uppercase name (`"INTEGER"`, `"REAL"`, `"TEXT"`,
171/// `"NUMERIC"`, `"BLOB"`) or `None` when no determination can be made.
172///
173/// Used by [`Statement::get_column_type_info`] to give wire-protocol layers
174/// a usable type for `SELECT 1+1`-style result columns. Goes beyond SQLite's
175/// `get_expr_affinity` (which deliberately stops at binary operators because
176/// SQLite's affinity model is about *column* coercion, not expression
177/// inference) by walking through arithmetic, bitwise, comparison, logical,
178/// and concat operators — letting `SELECT 42 + 1` report INT4 to a
179/// PostgreSQL client the way PG itself does.
180fn infer_expression_primitive(
181    expr: &turso_parser::ast::Expr,
182    referenced_tables: Option<&translate::plan::TableReferences>,
183) -> Option<&'static str> {
184    use turso_parser::ast::{Expr, Operator};
185
186    match expr {
187        // Bare literal: read the parsed concrete value type.
188        Expr::Literal(lit) => match translate::alter::literal_default_value(lit)
189            .ok()?
190            .value_type()
191        {
192            crate::types::ValueType::Integer => Some("INTEGER"),
193            crate::types::ValueType::Float => Some("REAL"),
194            crate::types::ValueType::Text => Some("TEXT"),
195            _ => None,
196        },
197        Expr::Parenthesized(exprs) if exprs.len() == 1 => {
198            infer_expression_primitive(exprs.first().unwrap(), referenced_tables)
199        }
200        Expr::Collate(inner, _) => infer_expression_primitive(inner, referenced_tables),
201        Expr::Unary(_, inner) => {
202            // Unary +/-/NOT preserve the operand's primitive (NOT on INTEGER
203            // is still INTEGER in SQLite).
204            infer_expression_primitive(inner, referenced_tables)
205        }
206        Expr::Binary(left, op, right) => match op {
207            // Arithmetic: widen INTEGER × INTEGER to INTEGER, anything mixed
208            // with REAL becomes REAL, fall through to NUMERIC otherwise.
209            Operator::Add
210            | Operator::Subtract
211            | Operator::Multiply
212            | Operator::Divide
213            | Operator::Modulus => {
214                let l = infer_expression_primitive(left, referenced_tables);
215                let r = infer_expression_primitive(right, referenced_tables);
216                Some(combine_arithmetic_primitive(l, r))
217            }
218            // Bitwise: result is always INTEGER in both SQLite and PG.
219            Operator::BitwiseAnd
220            | Operator::BitwiseOr
221            | Operator::BitwiseNot
222            | Operator::LeftShift
223            | Operator::RightShift => Some("INTEGER"),
224            // Comparison and logical: SQLite returns 0/1 INTEGER; pgmicro
225            // maps INTEGER to BOOL at the wire layer for boolean columns,
226            // but the type the wire layer reports is still INTEGER here.
227            Operator::Equals
228            | Operator::NotEquals
229            | Operator::Less
230            | Operator::LessEquals
231            | Operator::Greater
232            | Operator::GreaterEquals
233            | Operator::Is
234            | Operator::IsNot
235            | Operator::And
236            | Operator::Or
237            | Operator::ArrayContains
238            | Operator::ArrayOverlap => Some("INTEGER"),
239            // Concat is always TEXT.
240            Operator::Concat => Some("TEXT"),
241            // JSON ops fall through to the affinity machinery — `->` returns
242            // JSON / blob, `->>` returns TEXT; the existing affinity rules
243            // give the correct answer.
244            Operator::ArrowRight | Operator::ArrowRightShift => affinity_to_primitive(
245                translate::expr::get_expr_affinity(expr, referenced_tables, None),
246            ),
247        },
248        Expr::RowId { .. } => Some("INTEGER"),
249        // CAST, column references, and anything else: defer to the affinity
250        // machinery, which handles these shapes correctly.
251        _ => affinity_to_primitive(translate::expr::get_expr_affinity(
252            expr,
253            referenced_tables,
254            None,
255        )),
256    }
257}
258
259/// Map [`crate::vdbe::affinity::Affinity`] to the uppercase primitive name
260/// `infer_expression_primitive` returns. `Blob` collapses to `None` because
261/// SQLite's "no determined affinity" sentinel isn't a usable wire type.
262fn affinity_to_primitive(affinity: crate::vdbe::affinity::Affinity) -> Option<&'static str> {
263    match affinity {
264        crate::vdbe::affinity::Affinity::Integer => Some("INTEGER"),
265        crate::vdbe::affinity::Affinity::Real => Some("REAL"),
266        crate::vdbe::affinity::Affinity::Text => Some("TEXT"),
267        crate::vdbe::affinity::Affinity::Numeric => Some("NUMERIC"),
268        crate::vdbe::affinity::Affinity::Blob => None,
269    }
270}
271
272/// Pick the widening primitive for an arithmetic binary op given each
273/// operand's inferred primitive. `INTEGER + INTEGER -> INTEGER`,
274/// `INTEGER + REAL -> REAL`, everything else collapses to `NUMERIC` (the
275/// safe wire default for a mixed-affinity numeric result).
276fn combine_arithmetic_primitive(
277    left: Option<&'static str>,
278    right: Option<&'static str>,
279) -> &'static str {
280    match (left, right) {
281        (Some("INTEGER"), Some("INTEGER")) => "INTEGER",
282        (Some("INTEGER"), Some("REAL"))
283        | (Some("REAL"), Some("INTEGER"))
284        | (Some("REAL"), Some("REAL")) => "REAL",
285        _ => "NUMERIC",
286    }
287}
288
289pub struct Statement {
290    pub(crate) program: vdbe::Program,
291    state: vdbe::ProgramState,
292    pager: Arc<Pager>,
293    /// indicates if the statement is a NORMAL/EXPLAIN/EXPLAIN QUERY PLAN
294    query_mode: QueryMode,
295    /// Flag to show if the statement was busy
296    busy: bool,
297    /// Busy handler state for tracking invocations and timeouts
298    busy_handler_state: Option<BusyHandlerState>,
299    /// Per-execution timeout override for this statement.
300    /// - `None`: use connection default
301    /// - `Some(Some(duration))`: override with a query-specific timeout
302    /// - `Some(None)`: disable timeout for this execution
303    query_timeout_override: Option<Option<Duration>>,
304    /// True once step() has returned Row for a write statement (INSERT/UPDATE/DELETE
305    /// with RETURNING). With ephemeral-buffered RETURNING, the first Row proves all
306    /// DML completed — only the scan-back remains. Used by reset_internal to decide
307    /// commit vs rollback when a statement is abandoned.
308    has_returned_row: bool,
309    /// Byte offset in the original SQL string where this statement ends.
310    /// Used by sqlite3_prepare_v2 to set the *pzTail output parameter.
311    tail_offset: usize,
312    origin: StatementOrigin,
313    /// True once this root statement has started executing and incremented
314    /// `Connection::n_active_root_statements`.
315    counted_as_active_root: bool,
316    /// True if this statement called `Connection::start_nested()` during
317    /// construction and therefore must call `end_nested()` on drop.
318    nested_guard_active: bool,
319}
320
321crate::assert::assert_send_sync!(Statement);
322
323impl std::fmt::Debug for Statement {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        f.debug_struct("Statement").finish()
326    }
327}
328
329impl Statement {
330    pub fn new(
331        program: vdbe::Program,
332        pager: Arc<Pager>,
333        query_mode: QueryMode,
334        tail_offset: usize,
335    ) -> Self {
336        Self::new_with_origin(
337            program,
338            pager,
339            query_mode,
340            tail_offset,
341            StatementOrigin::Root,
342            false,
343        )
344    }
345
346    #[turso_macros::trace_stack]
347    pub(crate) fn new_with_origin(
348        program: vdbe::Program,
349        pager: Arc<Pager>,
350        query_mode: QueryMode,
351        tail_offset: usize,
352        origin: StatementOrigin,
353        nested_guard_active: bool,
354    ) -> Self {
355        let (max_registers, cursor_count) = match query_mode {
356            QueryMode::Normal => (program.max_registers, program.cursor_ref.len()),
357            QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0),
358            QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0),
359        };
360        let state = vdbe::ProgramState::new(max_registers, cursor_count);
361        Self {
362            program,
363            state,
364            pager,
365            query_mode,
366            busy: false,
367            busy_handler_state: None,
368            query_timeout_override: None,
369            has_returned_row: false,
370            tail_offset,
371            origin,
372            counted_as_active_root: false,
373            nested_guard_active,
374        }
375    }
376
377    pub fn tail_offset(&self) -> usize {
378        self.tail_offset
379    }
380
381    pub fn get_trigger(&self) -> Option<Arc<Trigger>> {
382        self.program.trigger.clone()
383    }
384
385    pub fn get_query_mode(&self) -> QueryMode {
386        self.query_mode
387    }
388
389    pub fn get_program(&self) -> &vdbe::Program {
390        &self.program
391    }
392
393    pub fn get_pager(&self) -> &Arc<Pager> {
394        &self.pager
395    }
396
397    pub fn n_change(&self) -> i64 {
398        self.state
399            .n_change
400            .load(crate::sync::atomic::Ordering::SeqCst)
401    }
402
403    pub fn n_total_change(&self) -> i64 {
404        self.state
405            .n_total_change
406            .load(crate::sync::atomic::Ordering::SeqCst)
407    }
408
409    pub fn set_mv_tx(&mut self, mv_tx: Option<(u64, TransactionMode)>) {
410        self.program.connection.set_mv_tx(mv_tx);
411    }
412
413    pub fn interrupt(&mut self) {
414        self.state.interrupt();
415    }
416
417    /// Sets a per-execution timeout override for this statement.
418    ///
419    /// - `None`: use connection default
420    /// - `Some(Some(duration))`: use query-specific timeout
421    /// - `Some(None)`: disable timeout for this execution
422    pub fn set_query_timeout_override(&mut self, timeout: Option<Option<Duration>>) {
423        self.query_timeout_override = timeout;
424    }
425
426    pub fn execution_state(&self) -> ProgramExecutionState {
427        self.state.execution_state
428    }
429
430    /// Statement metrics accumulated across executions of this prepared
431    /// statement. Includes subprogram work.
432    pub fn metrics(&self) -> vdbe::metrics::StatementMetrics {
433        self.state.metrics()
434    }
435
436    pub fn reset_metrics(&mut self) {
437        self.state.reset_metrics();
438    }
439
440    pub fn stmt_status(&self, counter: StatementStatusCounter) -> u64 {
441        let metrics = self.metrics();
442        match counter {
443            StatementStatusCounter::FullscanStep => metrics.fullscan_steps,
444            StatementStatusCounter::Sort => metrics.sort_operations,
445            StatementStatusCounter::VmStep => metrics.insn_executed,
446            StatementStatusCounter::Reprepare => metrics.reprepares,
447            StatementStatusCounter::RowsRead => metrics.rows_read,
448            StatementStatusCounter::RowsWritten => metrics.rows_written,
449        }
450    }
451
452    pub fn reset_stmt_status(&mut self, counter: StatementStatusCounter) {
453        self.state.reset_stmt_status(counter);
454    }
455
456    pub fn mv_store(&self) -> impl Deref<Target = Option<Arc<MvStore>>> {
457        self.program.connection.mv_store()
458    }
459
460    /// Take the pending IO completions from this statement.
461    /// Returns None if no IO is pending.
462    /// This is used by async state machines that need to yield the completions.
463    pub fn take_io_completions(&mut self) -> Option<crate::types::IOCompletions> {
464        self.state.io_completions.take()
465    }
466
467    fn arm_query_timeout_if_needed(&mut self) {
468        if !matches!(self.state.execution_state, ProgramExecutionState::Init)
469            || self.state.query_deadline.is_some()
470        {
471            return;
472        }
473        let timeout = match self.query_timeout_override {
474            Some(timeout_override) => timeout_override,
475            None => {
476                let connection_timeout = self.program.connection.get_query_timeout();
477                if connection_timeout.is_zero() {
478                    None
479                } else {
480                    Some(connection_timeout)
481                }
482            }
483        };
484        let Some(timeout) = timeout else {
485            return;
486        };
487        self.state.query_deadline = Some(self.pager.io.current_time_monotonic() + timeout);
488    }
489
490    fn release_active_root_if_counted(&mut self) {
491        if self.counted_as_active_root {
492            let previous = self
493                .program
494                .connection
495                .n_active_root_statements
496                .fetch_sub(1, Ordering::SeqCst);
497            if previous == 1 {
498                self.program.connection.clear_interrupt_if_idle();
499            }
500            self.counted_as_active_root = false;
501        }
502    }
503
504    fn _step(&mut self, waker: Option<&Waker>) -> Result<StepResult> {
505        if !self.counted_as_active_root && matches!(self.origin, StatementOrigin::Root) {
506            self.program
507                .connection
508                .n_active_root_statements
509                .fetch_add(1, Ordering::SeqCst);
510            self.counted_as_active_root = true;
511        }
512        if matches!(self.state.execution_state, ProgramExecutionState::Init)
513            && self.origin != StatementOrigin::InternalHelper
514        {
515            if self.program.connection.mvcc_enabled() {
516                // MVCC checkpoints can publish internal schema roots without changing
517                // SQLite's schema cookie, so refresh before deciding whether to reprepare.
518                self.program.connection.maybe_update_schema();
519            }
520            if !self
521                .program
522                .prepare_context
523                .matches_connection(&self.program.connection)
524            {
525                if let Err(err) = self.reprepare() {
526                    self.release_active_root_if_counted();
527                    return Err(err);
528                }
529            }
530        }
531
532        self.arm_query_timeout_if_needed();
533
534        // If we're waiting for a busy handler timeout, check if we can proceed
535        if let Some(busy_state) = self.busy_handler_state.as_ref() {
536            if self.pager.io.current_time_monotonic() < busy_state.timeout() {
537                // Yield the query as the timeout has not been reached yet
538                if let Some(waker) = waker {
539                    waker.wake_by_ref();
540                }
541                return Ok(StepResult::IO);
542            }
543        }
544
545        const MAX_SCHEMA_RETRY: usize = 50;
546        let mut res = self
547            .program
548            .step(&mut self.state, &self.pager, self.query_mode, waker);
549        for attempt in 0..MAX_SCHEMA_RETRY {
550            // Only reprepare if we still need to update schema
551            if !matches!(res, Err(LimboError::SchemaUpdated)) {
552                break;
553            }
554            // In a write transaction, reprepare may not help (e.g. cross-process
555            // schema change where the in-memory schema hasn't been refreshed from
556            // disk). Allow a few retries for the in-process case where reprepare
557            // *can* resolve the issue, but bail early to avoid burning 50 attempts.
558            if attempt >= 2
559                && !self.program.connection.get_auto_commit()
560                && matches!(
561                    self.program.connection.get_tx_state(),
562                    TransactionState::Write { .. } | TransactionState::PendingUpgrade { .. }
563                )
564            {
565                break;
566            }
567            tracing::debug!("reprepare: attempt={}", attempt);
568            if let Err(err) = self.reprepare() {
569                self.release_active_root_if_counted();
570                return Err(err);
571            }
572            res = self
573                .program
574                .step(&mut self.state, &self.pager, self.query_mode, waker);
575        }
576
577        // Aggregate metrics when statement completes
578        if matches!(res, Ok(StepResult::Done)) {
579            self.program
580                .connection
581                .metrics
582                .write()
583                .record_statement(&self.metrics());
584            self.busy = false;
585            self.busy_handler_state = None; // Reset busy state on completion
586            self.state.query_deadline = None;
587
588            // After ANALYZE completes, refresh in-memory stats so planners can use them.
589            let sql = self.program.sql.trim_start().as_bytes();
590            if sql.len() >= 7 && sql[..7].eq_ignore_ascii_case(b"ANALYZE") {
591                // The stats refresh runs a SELECT on this same connection. At
592                // this point ANALYZE is already Done, so it must not count as a
593                // sibling root statement for that internal SELECT.
594                self.release_active_root_if_counted();
595                refresh_analyze_stats(&self.program.connection);
596            }
597        } else {
598            self.busy = true;
599        }
600
601        // Handle busy result by invoking the busy handler
602        if matches!(res, Ok(StepResult::Busy)) {
603            let now = self.pager.io.current_time_monotonic();
604            let handler = self.program.connection.get_busy_handler();
605
606            // Initialize or get existing busy handler state
607            let busy_state = self
608                .busy_handler_state
609                .get_or_insert_with(|| BusyHandlerState::new(now));
610
611            // Invoke the busy handler to determine if we should retry
612            if busy_state.invoke(&handler, now) {
613                // Handler says retry, yield with IO to wait for timeout
614                if let Some(waker) = waker {
615                    waker.wake_by_ref();
616                }
617                res = Ok(StepResult::IO);
618                #[cfg(shuttle)]
619                crate::thread::spin_loop();
620            }
621            // else: Handler says stop, res stays as Busy
622        }
623
624        // Track when a write statement yields its first Row. With ephemeral-buffered
625        // RETURNING, this proves all DML completed — only the scan-back remains.
626        if matches!(res, Ok(StepResult::Row))
627            && self.query_mode == QueryMode::Normal
628            && self.program.change_cnt_on
629            && !self.program.result_columns.is_empty()
630        {
631            self.has_returned_row = true;
632        }
633
634        if self.counted_as_active_root
635            && (matches!(res, Ok(StepResult::Done | StepResult::Interrupt)) || res.is_err())
636        {
637            self.release_active_root_if_counted();
638        }
639
640        // If the bytecode aborted between SequenceBeginInnerTx and
641        // SequenceCommitInnerTx, the connection's mv_tx is still pointing
642        // at the orphan inner; subsequent statements (e.g. reparse_schema
643        // SELECTs from _step's own reprepare path) would inherit it and
644        // deadlock in commit_txn's WaitForDependencies. Roll back and
645        // restore the outer eagerly here — reset_internal alone is not
646        // enough because callers do not always reset on error before
647        // running another statement.
648        if res.is_err() {
649            self.cleanup_orphaned_seq_inner_tx();
650        }
651
652        res
653    }
654
655    #[inline]
656    pub fn step(&mut self) -> Result<StepResult> {
657        self._step(None)
658    }
659
660    #[inline]
661    pub fn step_with_waker(&mut self, waker: &Waker) -> Result<StepResult> {
662        self._step(Some(waker))
663    }
664
665    /// Fast step for trigger/FK subprograms: skips reprepare checks, timeout
666    /// arming, busy handler, metrics recording, and schema retry.
667    /// The parent statement handles all of those concerns.
668    #[inline]
669    pub fn step_subprogram(&mut self) -> Result<StepResult> {
670        self.program
671            .step(&mut self.state, &self.pager, self.query_mode, None)
672    }
673
674    pub fn run_ignore_rows(&mut self) -> Result<()> {
675        loop {
676            match self.step()? {
677                vdbe::StepResult::Done => return Ok(()),
678                vdbe::StepResult::IO | vdbe::StepResult::Yield => self.pager.io.step()?,
679                vdbe::StepResult::Row => continue,
680                vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => {
681                    return Err(LimboError::Busy)
682                }
683            }
684        }
685    }
686
687    pub fn run_collect_rows(&mut self) -> Result<Vec<Vec<Value>>> {
688        let mut values = Vec::new();
689        loop {
690            match self.step()? {
691                vdbe::StepResult::Done => return Ok(values),
692                vdbe::StepResult::IO | vdbe::StepResult::Yield => self.pager.io.step()?,
693                vdbe::StepResult::Row => {
694                    values.push(self.row().unwrap().get_values().cloned().collect());
695                    continue;
696                }
697                vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => {
698                    return Err(LimboError::Busy)
699                }
700            }
701        }
702    }
703
704    /// Blocks execution, advances IO, and runs to completion of the statement
705    pub fn run_with_row_callback(
706        &mut self,
707        mut func: impl FnMut(&Row) -> Result<()>,
708    ) -> Result<()> {
709        loop {
710            match self.step()? {
711                vdbe::StepResult::Done => break,
712                vdbe::StepResult::IO | vdbe::StepResult::Yield => self.pager.io.step()?,
713                vdbe::StepResult::Row => {
714                    func(self.row().expect("row should be present"))?;
715                }
716                vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt),
717                vdbe::StepResult::Busy => return Err(LimboError::Busy),
718            }
719        }
720        Ok(())
721    }
722
723    /// Non-blocking counterpart of [`Self::run_ignore_rows`]: drives the
724    /// statement to completion, ignoring rows, but instead of pumping IO
725    /// synchronously it yields the pending completion to the caller. Re-invoke
726    /// after the yielded completion finishes; the program resumes at the same
727    /// pc. Rows are discarded.
728    ///
729    /// Used by engine-internal callers that must stay non-blocking (MVCC
730    /// bootstrap/recovery) so they don't call `io.step()` on backends that have
731    /// no synchronous IO pump (e.g. WASM).
732    pub fn run_ignore_rows_nonblock(&mut self) -> Result<crate::IOResult<()>> {
733        loop {
734            match self.step()? {
735                vdbe::StepResult::Done => return Ok(crate::IOResult::Done(())),
736                vdbe::StepResult::Row => continue,
737                vdbe::StepResult::IO | vdbe::StepResult::Yield => {
738                    let io = self.take_io_completions().unwrap_or_else(|| {
739                        crate::types::IOCompletions::Single(crate::io::Completion::new_yield())
740                    });
741                    return Ok(crate::IOResult::IO(io));
742                }
743                vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt),
744                vdbe::StepResult::Busy => return Err(LimboError::Busy),
745            }
746        }
747    }
748
749    /// Non-blocking counterpart of [`Self::run_with_row_callback`]: drives the
750    /// statement to completion, invoking `func` once per emitted row, but
751    /// yields the pending completion to the caller instead of pumping IO
752    /// synchronously.
753    ///
754    /// Re-entrancy: on an IO yield the program is paused mid-opcode (never
755    /// between emitting a row and this loop observing it), so on re-invocation
756    /// stepping resumes without replaying the last row — every row's `func`
757    /// runs exactly once. Because the runner restarts from the top on each
758    /// re-entry, `func` must append to caller-owned state that persists across
759    /// yields (e.g. a field in the driving state machine), not to a local.
760    pub fn run_with_row_callback_nonblock(
761        &mut self,
762        mut func: impl FnMut(&Row) -> Result<()>,
763    ) -> Result<crate::IOResult<()>> {
764        loop {
765            match self.step()? {
766                vdbe::StepResult::Done => return Ok(crate::IOResult::Done(())),
767                vdbe::StepResult::Row => {
768                    func(self.row().expect("row should be present"))?;
769                }
770                vdbe::StepResult::IO | vdbe::StepResult::Yield => {
771                    let io = self.take_io_completions().unwrap_or_else(|| {
772                        crate::types::IOCompletions::Single(crate::io::Completion::new_yield())
773                    });
774                    return Ok(crate::IOResult::IO(io));
775                }
776                vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt),
777                vdbe::StepResult::Busy => return Err(LimboError::Busy),
778            }
779        }
780    }
781
782    /// Blocks execution, advances IO, and stops at any StepResult except IO
783    /// You can optionally pass a handler to run after IO is advanced
784    pub fn run_one_step_blocking(
785        &mut self,
786        mut pre_io_func: impl FnMut() -> Result<()>,
787        mut post_io_func: impl FnMut() -> Result<()>,
788    ) -> Result<Option<&Row>> {
789        let result = loop {
790            match self.step()? {
791                vdbe::StepResult::Done => break None,
792                vdbe::StepResult::IO | vdbe::StepResult::Yield => {
793                    pre_io_func()?;
794                    self.pager.io.step()?;
795                    post_io_func()?;
796                }
797                vdbe::StepResult::Row => break Some(self.row().expect("row should be present")),
798                vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt),
799                vdbe::StepResult::Busy => return Err(LimboError::Busy),
800            }
801        };
802        Ok(result)
803    }
804
805    #[instrument(skip_all, level = Level::DEBUG)]
806    fn reprepare(&mut self) -> Result<()> {
807        tracing::trace!("repreparing statement");
808        let conn = self.program.connection.clone();
809        let main_pager = conn.pager.load().clone();
810
811        // SchemaUpdated bypasses the normal abort rollback path, so in
812        // autocommit mode we must unwind any implicit transaction state here
813        // before reparsing. This must clear both pager locks and MVCC tx ids;
814        // otherwise the retried statement can stack a fresh snapshot on top of
815        // leaked transaction state from the failed attempt.
816        let attached_leaked = conn.with_all_attached_pagers_with_index(|pagers| {
817            pagers
818                .iter()
819                .any(|(_, pager)| pager.holds_write_lock() || pager.holds_read_lock())
820        });
821        let has_implicit_txn_state = conn.get_tx_state() != TransactionState::None
822            || conn.get_mv_tx().is_some()
823            || conn.next_attached_mv_tx().is_some()
824            || attached_leaked
825            || self.state.auto_txn_cleanup != vdbe::TxnCleanup::None;
826        if conn.get_auto_commit() && has_implicit_txn_state {
827            conn.rollback_current_txn_state(&main_pager, true);
828            self.state.auto_txn_cleanup = vdbe::TxnCleanup::None;
829        }
830        if conn.get_auto_commit() && !conn.schema_reparse_in_progress() {
831            conn.maybe_reparse_schema()?;
832        }
833
834        // End transactions on attached database pagers so they get a fresh view
835        // of the database. Without this, the pager would still see the old page 1
836        // with the stale schema cookie, causing an infinite SchemaUpdated loop.
837        // SchemaUpdated can occur at different points in the Transaction opcode,
838        // so the attached pager may or may not hold locks at this point.
839        let attached_db_ids: BitSet = self
840            .program
841            .prepared
842            .write_databases
843            .iter()
844            .chain(self.program.prepared.read_databases.iter())
845            .filter(|&id| id != crate::MAIN_DB_ID)
846            .try_collect()?;
847        for db_id in &attached_db_ids {
848            // Reprepare must not roll back an explicit transaction. SQLite allows
849            // reprepare inside a transaction, and uncommitted writes in temp or
850            // attached databases remain visible after the statement is retried.
851            if db_id == crate::TEMP_DB_ID || !conn.get_auto_commit() {
852                continue;
853            }
854            // Discard any connection-local schema changes for this attached DB
855            // so the re-translate reads the committed schema.
856            conn.database_schemas().write().remove(&db_id);
857            let pager = conn.get_pager_from_database_index(&db_id)?;
858            if pager.holds_read_lock() {
859                pager.rollback_attached();
860            }
861        }
862
863        // Refresh from shared schema only when shared is newer; this preserves a
864        // connection-local schema that is ahead of shared. An MVCC checkpoint can
865        // publish new btree roots without bumping the schema cookie, so
866        // same-version reprepare still refreshes it.
867        conn.refresh_schema_from_shared_for_reprepare();
868        let new_program = {
869            let mut parser = Parser::new(self.program.sql.as_bytes());
870            let cmd = parser.next_cmd()?;
871            let cmd = cmd.expect("Same SQL string should be able to be parsed");
872
873            let syms = conn.syms.read();
874            let mode = self.query_mode;
875            #[cfg(debug_assertions)]
876            crate::turso_assert_eq!(QueryMode::new(&cmd), mode);
877            let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd;
878            let schema = conn.schema.read().clone();
879            translate::translate(
880                &schema,
881                stmt,
882                self.pager.clone(),
883                conn.clone(),
884                &syms,
885                mode,
886                &self.program.sql,
887            )?
888        };
889
890        // Save parameters before they are reset
891        let parameters = std::mem::take(&mut self.state.parameters);
892        let (max_registers, cursor_count) = match self.query_mode {
893            QueryMode::Normal => (new_program.max_registers, new_program.cursor_ref.len()),
894            QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0),
895            QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0),
896        };
897        // Repreparing a root statement must not make it disappear from
898        // `n_active_root_statements` while it is still logically in progress.
899        self.reset_internal(
900            Some(max_registers),
901            Some(cursor_count),
902            self.counted_as_active_root,
903        )?;
904        self.state.metrics.reprepares = self.state.metrics.reprepares.saturating_add(1);
905        self.program = new_program;
906        // Load the parameters back into the state
907        self.state.parameters = parameters;
908        Ok(())
909    }
910
911    pub fn num_columns(&self) -> usize {
912        match self.query_mode {
913            QueryMode::Normal => self.program.result_columns.len(),
914            QueryMode::Explain => EXPLAIN_COLUMNS.len(),
915            QueryMode::ExplainQueryPlan => EXPLAIN_QUERY_PLAN_COLUMNS.len(),
916        }
917    }
918
919    pub fn get_column_name(&self, idx: usize) -> Cow<'_, str> {
920        if self.query_mode == QueryMode::Explain {
921            return Cow::Owned(EXPLAIN_COLUMNS.get(idx).expect("No column").to_string());
922        }
923        if self.query_mode == QueryMode::ExplainQueryPlan {
924            return Cow::Owned(
925                EXPLAIN_QUERY_PLAN_COLUMNS
926                    .get(idx)
927                    .expect("No column")
928                    .to_string(),
929            );
930        }
931        match self.query_mode {
932            QueryMode::Normal => {
933                let column = &self.program.result_columns.get(idx).expect("No column");
934
935                // 1. Explicit alias (AS clause) or SELECT * expansion always wins.
936                if let Some(alias) = &column.alias {
937                    return Cow::Borrowed(alias);
938                }
939
940                let full = self.program.connection.get_full_column_names();
941                let short = self.program.connection.get_short_column_names();
942
943                // 2. For column references, apply full/short column name logic.
944                match &column.expr {
945                    turso_parser::ast::Expr::Column {
946                        table,
947                        column: col_idx,
948                        ..
949                    } => {
950                        if full {
951                            // full_column_names=ON: use REAL_TABLE_NAME.COLUMN
952                            if let Some((_, table_ref)) = self
953                                .program
954                                .table_references
955                                .find_table_by_internal_id(*table)
956                            {
957                                let col_name = table_ref
958                                    .get_column_at(*col_idx)
959                                    .and_then(|c| c.name.as_deref())
960                                    .unwrap_or("?");
961                                return Cow::Owned(format!(
962                                    "{}.{}",
963                                    table_ref.get_name(),
964                                    col_name
965                                ));
966                            }
967                        }
968                        if short || full {
969                            // short_column_names=ON: use just COLUMN
970                            if let Some(name) = column.name(&self.program.table_references) {
971                                return Cow::Borrowed(name);
972                            }
973                        }
974                        // Both OFF: use original expression text
975                        if let Some(name) = &column.implicit_column_name {
976                            Cow::Borrowed(name.as_str())
977                        } else {
978                            let tables = [&self.program.table_references];
979                            let ctx = PlanContext(&tables);
980                            Cow::Owned(column.expr.displayer(&ctx).to_string())
981                        }
982                    }
983                    _ => {
984                        // Non-column-ref: use implicit_column_name or displayer
985                        match column.name(&self.program.table_references) {
986                            Some(name) => Cow::Borrowed(name),
987                            None => {
988                                let tables = [&self.program.table_references];
989                                let ctx = PlanContext(&tables);
990                                Cow::Owned(column.expr.displayer(&ctx).to_string())
991                            }
992                        }
993                    }
994                }
995            }
996            QueryMode::Explain => Cow::Borrowed(EXPLAIN_COLUMNS[idx]),
997            QueryMode::ExplainQueryPlan => Cow::Borrowed(EXPLAIN_QUERY_PLAN_COLUMNS[idx]),
998        }
999    }
1000
1001    pub fn get_column_table_name(&self, idx: usize) -> Option<Cow<'_, str>> {
1002        if self.query_mode == QueryMode::Explain || self.query_mode == QueryMode::ExplainQueryPlan {
1003            return None;
1004        }
1005        let column = &self.program.result_columns.get(idx).expect("No column");
1006        match &column.expr {
1007            turso_parser::ast::Expr::Column { table, .. } => self
1008                .program
1009                .table_references
1010                .find_table_by_internal_id(*table)
1011                .map(|(_, table_ref)| Cow::Borrowed(table_ref.get_name())),
1012            _ => None,
1013        }
1014    }
1015
1016    /// Returns the declared type of a result column.
1017    ///
1018    /// This behaves similarly to SQLite's `sqlite3_column_decltype()`:
1019    /// If the Nth column of the returned result set of a SELECT is a table column
1020    /// (not an expression or subquery) then the declared type of the table column
1021    /// is returned. If the Nth column of the result set is an expression or subquery,
1022    /// then None is returned. The returned string is always UTF-8 encoded.
1023    ///
1024    /// See: <https://sqlite.org/c3ref/column_decltype.html>
1025    pub fn get_column_decltype(&self, idx: usize) -> Option<String> {
1026        if self.query_mode == QueryMode::Explain {
1027            return Some(
1028                EXPLAIN_COLUMNS_TYPE
1029                    .get(idx)
1030                    .expect("No column")
1031                    .to_string(),
1032            );
1033        }
1034        if self.query_mode == QueryMode::ExplainQueryPlan {
1035            return Some(
1036                EXPLAIN_QUERY_PLAN_COLUMNS_TYPE
1037                    .get(idx)
1038                    .expect("No column")
1039                    .to_string(),
1040            );
1041        }
1042        let column = &self.program.result_columns.get(idx).expect("No column");
1043        match &column.expr {
1044            turso_parser::ast::Expr::Column {
1045                table,
1046                column: column_idx,
1047                ..
1048            } => {
1049                let (_, table_ref) = self
1050                    .program
1051                    .table_references
1052                    .find_table_by_internal_id(*table)?;
1053                let table_column = table_ref.get_column_at(*column_idx)?;
1054                let ty_str = &table_column.ty_str;
1055                if ty_str.is_empty() {
1056                    None
1057                } else {
1058                    Some(ty_str.clone())
1059                }
1060            }
1061            _ => None,
1062        }
1063    }
1064
1065    /// Returns rich type information for a result column.
1066    ///
1067    /// This is Turso's single entry point for "what is the type of this
1068    /// column?" — covering both **direct table-column references** (where the
1069    /// schema carries declared name, array depth, custom-type kind, and the
1070    /// resolved primitive) and **computed expressions** (where the SQLite-
1071    /// style affinity machinery infers a primitive type from the expression
1072    /// shape). One call, one shape, regardless of which path applies.
1073    ///
1074    /// ### Return value
1075    ///
1076    /// - `Err(_)` when this connection does not have the experimental
1077    ///   custom-types feature enabled. This API is the public surface of the
1078    ///   custom-types system; callers must opt in by enabling
1079    ///   `--experimental-custom-types` (or `DatabaseOpts::with_custom_types`)
1080    ///   before they can rely on it.
1081    /// - `Ok(None)` when the statement is in EXPLAIN mode, when `idx` is out
1082    ///   of bounds, when the result column has no schema column behind it
1083    ///   AND the affinity machinery returns `BLOB` (i.e. "no determined
1084    ///   affinity"), or when a join/CTE reference can't be resolved.
1085    /// - `Ok(Some(info))` otherwise. For a table-column reference, `info`
1086    ///   carries the declared name verbatim; for an expression, `declared_name`
1087    ///   is the inferred-affinity primitive (`"INTEGER"`, `"TEXT"`, `"REAL"`,
1088    ///   or `"NUMERIC"`) and `kind` is `Builtin`.
1089    ///
1090    /// This is a Turso-specific API; it has no `sqlite3_*` counterpart. The
1091    /// returned struct is `#[non_exhaustive]` so additional metadata can be
1092    /// added over time without breaking callers.
1093    pub fn get_column_type_info(&self, idx: usize) -> Result<Option<ColumnTypeInfo>> {
1094        if !self.program.connection.experimental_custom_types_enabled() {
1095            return Err(LimboError::ParseError(
1096                "get_column_type_info requires --experimental-custom-types".to_string(),
1097            ));
1098        }
1099        if self.query_mode != QueryMode::Normal {
1100            return Ok(None);
1101        }
1102        let Some(column) = self.program.result_columns.get(idx) else {
1103            return Ok(None);
1104        };
1105        // Direct table-column reference: pull declared name, array depth, and
1106        // any registered CREATE TYPE / CREATE DOMAIN resolution out of the
1107        // schema. Anything else falls through to the expression-affinity
1108        // inference path below.
1109        if let turso_parser::ast::Expr::Column {
1110            table,
1111            column: column_idx,
1112            ..
1113        } = &column.expr
1114        {
1115            let Some((_, table_ref)) = self
1116                .program
1117                .table_references
1118                .find_table_by_internal_id(*table)
1119            else {
1120                return Ok(None);
1121            };
1122            let Some(table_column) = table_ref.get_column_at(*column_idx) else {
1123                return Ok(None);
1124            };
1125            let declared_name = table_column.ty_str.clone();
1126            let array_dimensions = table_column.array_dimensions();
1127            let schema = self.program.connection.schema.read();
1128            let resolved = schema
1129                .resolve_type(&declared_name, table_ref.is_strict())
1130                .ok()
1131                .flatten();
1132            // `kind` is computed from the leaf TypeDef in the resolution chain:
1133            // STRUCT and UNION are tagged on `TypeDefKind`, DOMAIN is tagged
1134            // separately on `TypeDef.is_domain`, and anything else registered
1135            // through CREATE TYPE is a Custom. A column whose declared name
1136            // does not appear in the type registry is a Builtin.
1137            let (base_type, kind) = match resolved {
1138                Some(resolved) => {
1139                    let leaf = resolved.leaf();
1140                    let kind = if leaf.is_struct() {
1141                        ColumnTypeKind::Struct
1142                    } else if leaf.is_union() {
1143                        ColumnTypeKind::Union
1144                    } else if leaf.is_domain {
1145                        ColumnTypeKind::Domain
1146                    } else {
1147                        ColumnTypeKind::Custom
1148                    };
1149                    (Some(resolved.primitive.to_uppercase()), kind)
1150                }
1151                None => (None, ColumnTypeKind::Builtin),
1152            };
1153            drop(schema);
1154            return Ok(Some(ColumnTypeInfo {
1155                declared_name,
1156                array_dimensions,
1157                base_type,
1158                kind,
1159            }));
1160        }
1161        // Not a table column: infer the result primitive from the
1162        // expression's shape (literal value type, operand types of a binary
1163        // op, the CAST target, etc.).
1164        let Some(name) =
1165            infer_expression_primitive(&column.expr, Some(&self.program.table_references))
1166        else {
1167            return Ok(None);
1168        };
1169        Ok(Some(ColumnTypeInfo {
1170            declared_name: name.to_string(),
1171            array_dimensions: 0,
1172            base_type: None,
1173            kind: ColumnTypeKind::Builtin,
1174        }))
1175    }
1176
1177    /// Returns the type affinity name of a result column (e.g., "INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC").
1178    ///
1179    /// Unlike `get_column_decltype` which returns the original declared type string,
1180    /// this method returns the normalized SQLite type affinity name.
1181    pub fn get_column_type_name(&self, idx: usize) -> Option<String> {
1182        if self.query_mode == QueryMode::Explain {
1183            return Some(
1184                EXPLAIN_COLUMNS_TYPE
1185                    .get(idx)
1186                    .expect("No column")
1187                    .to_string(),
1188            );
1189        }
1190        if self.query_mode == QueryMode::ExplainQueryPlan {
1191            return Some(
1192                EXPLAIN_QUERY_PLAN_COLUMNS_TYPE
1193                    .get(idx)
1194                    .expect("No column")
1195                    .to_string(),
1196            );
1197        }
1198        let column = &self.program.result_columns.get(idx).expect("No column");
1199        match &column.expr {
1200            turso_parser::ast::Expr::Column {
1201                table,
1202                column: column_idx,
1203                ..
1204            } => {
1205                let (_, table_ref) = self
1206                    .program
1207                    .table_references
1208                    .find_table_by_internal_id(*table)?;
1209                let table_column = table_ref.get_column_at(*column_idx)?;
1210                match &table_column.ty() {
1211                    crate::schema::Type::Integer => Some("INTEGER".to_string()),
1212                    crate::schema::Type::Real => Some("REAL".to_string()),
1213                    crate::schema::Type::Text => Some("TEXT".to_string()),
1214                    crate::schema::Type::Blob => Some("BLOB".to_string()),
1215                    crate::schema::Type::Numeric => Some("NUMERIC".to_string()),
1216                    crate::schema::Type::Null => None,
1217                }
1218            }
1219            _ => None,
1220        }
1221    }
1222
1223    pub fn parameters(&self) -> &parameters::Parameters {
1224        &self.program.parameters
1225    }
1226
1227    pub fn parameters_count(&self) -> usize {
1228        self.program.parameters.count()
1229    }
1230
1231    pub fn parameter_index(&self, name: &str) -> Option<NonZero<usize>> {
1232        self.program.parameters.index(name)
1233    }
1234
1235    pub fn bind_at(&mut self, index: NonZero<usize>, value: Value) -> Result<()> {
1236        self.state.bind_at(index, value)?;
1237        Ok(())
1238    }
1239
1240    pub fn clear_bindings(&mut self) {
1241        self.state.clear_bindings();
1242    }
1243
1244    pub fn reset(&mut self) -> Result<()> {
1245        self.reset_internal(None, None, false)
1246    }
1247
1248    /// If `Insn::SequenceBeginInnerTx` swapped the connection's mv_tx to
1249    /// an inner tx and the statement aborted before `SequenceCommitInnerTx`
1250    /// could clean it up, roll back the inner and restore the outer
1251    /// mv_tx. Otherwise the inner is leaked: it stays in `mv_store.txs`
1252    /// (so subsequent `commit_dep_counter` walks may wait on it forever)
1253    /// and the connection's mv_tx points to a dead tx, breaking the
1254    /// next statement that runs on the connection.
1255    fn cleanup_orphaned_seq_inner_tx(&mut self) {
1256        let Some(pending) = self.state.sequence_inner_tx_pending.take() else {
1257            return;
1258        };
1259        let conn = self.program.connection.clone();
1260        let Some(mv_store) = conn.mv_store_for_db(pending.db) else {
1261            return;
1262        };
1263        if mv_store.is_tx_rollbackable(pending.inner_tx_id) {
1264            mv_store.rollback_tx(pending.inner_tx_id, self.pager.clone(), &conn, pending.db);
1265        }
1266        conn.set_mv_tx_for_db(pending.db, pending.saved_outer);
1267        // When the inner tx aborted via the vdbe's catch-all error path
1268        // (e.g. DatabaseFull on sequence exhaustion), rollback_current_txn_state
1269        // rolled back what mv_tx pointed at — the inner — and set
1270        // auto_commit=true under the assumption it was the only live tx.
1271        // Restoring mv_tx to the outer without also restoring auto_commit=false
1272        // leaves the connection in an inconsistent state where auto_commit=true
1273        // but mv_tx points to a live outer tx, which causes subsequent BEGINs
1274        // to silently no-op and pins the caller to the outer's stale snapshot.
1275        if pending.saved_outer.is_some() {
1276            conn.auto_commit.store(false, Ordering::SeqCst);
1277        }
1278        // The commit-state-machine, if any was in flight, is now dead:
1279        // the inner tx it was committing is gone.
1280        self.state.sequence_inner_commit = None;
1281    }
1282
1283    pub fn reset_best_effort(&mut self) {
1284        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.reset())) {
1285            Ok(Ok(())) => {}
1286            Ok(Err(err)) => {
1287                tracing::error!("Statement reset failed during best-effort cleanup: {err}");
1288            }
1289            Err(_) => {
1290                tracing::error!("Statement reset panicked during best-effort cleanup");
1291            }
1292        }
1293    }
1294
1295    /// Lightweight reset for reusing a cached subprogram statement.
1296    /// Skips transaction handling and abort(): the caller (op_program) has
1297    /// already handled trigger execution tracking. Only resets ProgramState
1298    /// fields so the subprogram can run again from the beginning.
1299    pub fn reset_for_subprogram_reuse(&mut self) {
1300        self.cleanup_orphaned_seq_inner_tx();
1301        self.state.reset(None, None);
1302        self.state
1303            .n_change
1304            .store(0, std::sync::atomic::Ordering::Release);
1305        self.busy = false;
1306        self.has_returned_row = false;
1307    }
1308
1309    fn reset_internal(
1310        &mut self,
1311        max_registers: Option<usize>,
1312        max_cursors: Option<usize>,
1313        preserve_active_root_count: bool,
1314    ) -> Result<()> {
1315        fn capture_reset_error(
1316            reset_error: &mut Option<LimboError>,
1317            err: LimboError,
1318            context: &str,
1319        ) {
1320            tracing::error!("{context}: {err}");
1321            if reset_error.is_none() {
1322                *reset_error = Some(err);
1323            }
1324        }
1325
1326        let mut reset_error: Option<LimboError> = None;
1327
1328        if let Some(io) = self.state.io_completions.take() {
1329            if let Err(err) = io.wait(self.pager.io.as_ref()) {
1330                capture_reset_error(
1331                    &mut reset_error,
1332                    err,
1333                    "Error while draining pending IO during statement reset",
1334                );
1335            }
1336        }
1337
1338        if self.state.execution_state.is_running() {
1339            if self.query_mode == QueryMode::Normal
1340                && self.program.change_cnt_on
1341                && self.has_returned_row
1342            {
1343                // Write statement with RETURNING, user got at least one Row.
1344                // With ephemeral-buffered RETURNING, ALL DML completed before any
1345                // rows were yielded. The remaining work is just the scan-back
1346                // (in-memory) + Halt. Commit the transaction via halt().
1347                let mut halt_completed = false;
1348                loop {
1349                    match vdbe::execute::halt(
1350                        &self.program,
1351                        &mut self.state,
1352                        &self.pager,
1353                        0,
1354                        "",
1355                        None,
1356                    ) {
1357                        Ok(vdbe::execute::InsnFunctionStepResult::Done) => {
1358                            halt_completed = true;
1359                            break;
1360                        }
1361                        Ok(vdbe::execute::InsnFunctionStepResult::IO(_)) => {
1362                            if let Err(e) = self.pager.io.step() {
1363                                capture_reset_error(
1364                                    &mut reset_error,
1365                                    e,
1366                                    "Error committing during statement reset",
1367                                );
1368                                break;
1369                            }
1370                        }
1371                        Err(e) => {
1372                            capture_reset_error(
1373                                &mut reset_error,
1374                                e,
1375                                "Error halting statement during reset",
1376                            );
1377                            break;
1378                        }
1379                        Ok(vdbe::execute::InsnFunctionStepResult::Row)
1380                        | Ok(vdbe::execute::InsnFunctionStepResult::Step) => {
1381                            capture_reset_error(
1382                                &mut reset_error,
1383                                LimboError::InternalError(
1384                                    "Unexpected halt result during reset".to_string(),
1385                                ),
1386                                "Statement reset encountered unexpected halt result",
1387                            );
1388                            break;
1389                        }
1390                    }
1391                }
1392
1393                if !halt_completed {
1394                    if let Err(abort_err) =
1395                        self.program
1396                            .abort(&self.pager, reset_error.as_ref(), &mut self.state)
1397                    {
1398                        capture_reset_error(
1399                            &mut reset_error,
1400                            abort_err,
1401                            "Abort failed during statement reset",
1402                        );
1403                    }
1404                }
1405            } else {
1406                // Either a read-only statement, a write statement that never
1407                // yielded a Row (DML still in progress or hit Busy/error), or a
1408                // write statement without RETURNING. Rollback to avoid committing
1409                // partial DML or silently retrying after transient errors (Busy).
1410                if let Err(abort_err) = self.program.abort(&self.pager, None, &mut self.state) {
1411                    capture_reset_error(
1412                        &mut reset_error,
1413                        abort_err,
1414                        "Abort failed during statement reset",
1415                    );
1416                }
1417            }
1418        } else {
1419            // Statement not running (Done/Failed/Init) — cleanup only.
1420            if let Err(abort_err) = self.program.abort(&self.pager, None, &mut self.state) {
1421                capture_reset_error(
1422                    &mut reset_error,
1423                    abort_err,
1424                    "Abort failed during statement reset",
1425                );
1426            }
1427        }
1428        // Safety net: if end_statement wasn't reached (e.g. statement dropped
1429        // mid-execution), ensure n_active_writes is decremented before reset
1430        // clears the flag.
1431        if self.state.is_active_write {
1432            let previous = self
1433                .program
1434                .connection
1435                .n_active_writes
1436                .fetch_sub(1, Ordering::SeqCst);
1437            turso_assert!(
1438                previous == 1,
1439                "resetting a writer with {previous} active writer(s)"
1440            );
1441            self.state.is_active_write = false;
1442        }
1443        if self.counted_as_active_root && !preserve_active_root_count {
1444            self.release_active_root_if_counted();
1445        }
1446        self.cleanup_orphaned_seq_inner_tx();
1447        self.state.reset(max_registers, max_cursors);
1448        self.busy = false;
1449        self.busy_handler_state = None;
1450        self.query_timeout_override = None;
1451        self.has_returned_row = false;
1452
1453        if let Some(err) = reset_error {
1454            return Err(err);
1455        }
1456        Ok(())
1457    }
1458
1459    pub fn row(&self) -> Option<&Row> {
1460        self.state.result_row.as_ref()
1461    }
1462
1463    pub fn get_sql(&self) -> &str {
1464        &self.program.sql
1465    }
1466
1467    pub fn is_busy(&self) -> bool {
1468        self.busy
1469    }
1470
1471    /// Internal method to get IO from a statement.
1472    /// Used by select internal crate
1473    ///
1474    /// Avoid using this method for advancing IO while iteration over `step`.
1475    /// Prefer to use helper methods instead such as [Self::run_with_row_callback]
1476    pub fn _io(&self) -> &dyn crate::IO {
1477        self.pager.io.as_ref()
1478    }
1479}
1480
1481impl Drop for Statement {
1482    fn drop(&mut self) {
1483        // Keep helper statements nested while drop-time reset/abort cleanup runs.
1484        // That cleanup consults `is_nested_stmt()` to decide whether top-level
1485        // transaction/savepoint finalization belongs to this statement or to its
1486        // parent, so we release the nested guard only after reset completes.
1487        self.reset_best_effort();
1488        if self.nested_guard_active {
1489            self.program.connection.end_nested();
1490            self.nested_guard_active = false;
1491        }
1492    }
1493}
1494
1495#[cfg(clt_turso_tests)]
1496mod tests {
1497    use super::*;
1498    use crate::{Database, DatabaseOpts, MemoryIO, OpenFlags, IO};
1499
1500    fn open_test_connection() -> crate::Result<Arc<crate::Connection>> {
1501        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
1502        let db = Database::open_file_with_flags(
1503            io,
1504            ":memory:",
1505            OpenFlags::Create,
1506            DatabaseOpts::new(),
1507            None,
1508        )?;
1509        db.connect()
1510    }
1511
1512    #[test]
1513    fn test_metrics_persist_across_reset() {
1514        let conn = open_test_connection().unwrap();
1515        conn.execute("CREATE TABLE t(x)").unwrap();
1516        conn.metrics.write().reset();
1517
1518        let mut stmt = conn.prepare("INSERT INTO t VALUES (1)").unwrap();
1519        stmt.run_ignore_rows().unwrap();
1520        assert_eq!(stmt.metrics().rows_written, 1);
1521
1522        stmt.reset().unwrap();
1523        assert_eq!(stmt.metrics().rows_written, 1);
1524
1525        stmt.run_ignore_rows().unwrap();
1526        assert_eq!(stmt.metrics().rows_written, 2);
1527
1528        stmt.reset_metrics();
1529        assert_eq!(stmt.metrics().rows_written, 0);
1530    }
1531
1532    #[test]
1533    fn test_run_with_row_callback_nonblock_collects_all_rows() {
1534        let conn = open_test_connection().unwrap();
1535        conn.execute("CREATE TABLE t(x)").unwrap();
1536        conn.execute("INSERT INTO t VALUES (1), (2), (3), (4), (5)")
1537            .unwrap();
1538
1539        let io = conn.db.io.clone();
1540        let mut stmt = conn.prepare("SELECT x FROM t ORDER BY x").unwrap();
1541
1542        // Drive the non-blocking runner via the IOResult loop, exactly as a
1543        // state-machine caller would: collect into an accumulator that persists
1544        // across yields and wait on each yielded completion.
1545        let mut collected: Vec<i64> = Vec::new();
1546        loop {
1547            let res = stmt
1548                .run_with_row_callback_nonblock(|row| {
1549                    collected.push(row.get::<i64>(0)?);
1550                    Ok(())
1551                })
1552                .unwrap();
1553            match res {
1554                crate::IOResult::Done(()) => break,
1555                crate::IOResult::IO(c) => c.wait(io.as_ref()).unwrap(),
1556            }
1557        }
1558        assert_eq!(collected, vec![1, 2, 3, 4, 5]);
1559    }
1560
1561    #[test]
1562    fn test_run_ignore_rows_nonblock_completes() {
1563        let conn = open_test_connection().unwrap();
1564        conn.execute("CREATE TABLE t(x)").unwrap();
1565
1566        let io = conn.db.io.clone();
1567        let mut stmt = conn.prepare("INSERT INTO t VALUES (1), (2)").unwrap();
1568        loop {
1569            match stmt.run_ignore_rows_nonblock().unwrap() {
1570                crate::IOResult::Done(()) => break,
1571                crate::IOResult::IO(c) => c.wait(io.as_ref()).unwrap(),
1572            }
1573        }
1574        assert_eq!(stmt.metrics().rows_written, 2);
1575    }
1576
1577    #[test]
1578    fn test_metrics_include_subprogram_writes() {
1579        let conn = open_test_connection().unwrap();
1580        conn.execute("CREATE TABLE src(x)").unwrap();
1581        conn.execute("CREATE TABLE log(x)").unwrap();
1582        conn.execute(
1583            "CREATE TRIGGER src_log AFTER INSERT ON src BEGIN INSERT INTO log VALUES (new.x); END",
1584        )
1585        .unwrap();
1586
1587        let mut stmt = conn.prepare("INSERT INTO src VALUES (1), (2)").unwrap();
1588        stmt.run_ignore_rows().unwrap();
1589
1590        assert_eq!(
1591            stmt.metrics().rows_written,
1592            6,
1593            "cumulative metrics should include root and trigger writes"
1594        );
1595    }
1596}