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) -> ¶meters::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}