Skip to main content

clt_database/
connection.rs

1use crate::alloc::TryClone;
2use crate::error::io_error;
3#[cfg(any(clt_turso_tests, injected_yields))]
4use crate::mvcc::yield_points::{FailureInjector, YieldInjector};
5use crate::statement::StatementOrigin;
6use crate::storage::{journal_mode, pager::SavepointResult};
7use crate::sync::{
8    atomic::{
9        AtomicBool, AtomicI32, AtomicI64, AtomicIsize, AtomicU16, AtomicU64, AtomicU8, Ordering,
10    },
11    Arc, RwLock,
12};
13#[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
14use crate::types::{WalFrameInfo, WalState};
15#[cfg(clt_turso_feature = "fs")]
16use crate::util::{OpenMode, OpenOptions};
17#[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
18use crate::Page;
19use crate::{
20    ast, function,
21    io::{MemoryIO, IO},
22    progress::{ProgressHandler, ProgressHandlerCallback},
23    translate,
24    translate::collate::CollationSeq,
25    util::IOExt,
26    vdbe, AllViewsTxState, AtomicCipherMode, AtomicSyncMode, AtomicTempStore, BusyHandler,
27    BusyHandlerCallback, CaptureDataChangesInfo, CheckpointMode, CheckpointResult, CipherMode, Cmd,
28    Completion, ConnectionMetrics, Database, DatabaseCatalog, DatabaseOpts, Duration,
29    EncryptionKey, EncryptionOpts, IOResult, IndexMethod, LimboError, MvStore, OpenFlags, PageSize,
30    Pager, Parser, Program, QueryMode, QueryRunner, Result, Schema, Statement, SyncMode,
31    TransactionMode, Trigger, Value, VirtualTable, WalAutoActions,
32};
33use crate::{is_memory_like, turso_assert};
34use crate::{MAIN_DB_ID, TEMP_DB_ID};
35use arc_swap::ArcSwap;
36use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
37use smallvec::SmallVec;
38use std::cmp::Ordering as CmpOrdering;
39use std::fmt::Display;
40use std::ops::Deref;
41#[cfg(clt_turso_feature = "simulator")]
42use std::path::Path;
43#[cfg(not(target_family = "wasm"))]
44use tempfile::TempDir;
45use tracing::{instrument, Level};
46use turso_macros::{turso_assert_ne, AtomicEnum};
47
48#[cfg(clt_turso_feature = "simulator")]
49fn db_identity_for_testing(db_path: &Path) -> Result<(u32, u32)> {
50    let bytes =
51        std::fs::read(db_path).map_err(|e| io_error(e, "read db header for simulator testing"))?;
52    let db_header_size = crate::storage::sqlite3_ondisk::DatabaseHeader::SIZE;
53    if bytes.len() < db_header_size {
54        return Err(LimboError::InternalError(format!(
55            "database file is smaller than the header: got {}, need at least {}",
56            bytes.len(),
57            db_header_size
58        )));
59    }
60    let db_size_pages = u32::from_be_bytes(bytes[28..32].try_into().unwrap());
61    let crc = crc32c::crc32c(&bytes[..db_header_size]);
62    Ok((db_size_pages, crc))
63}
64
65#[derive(Clone, AtomicEnum, Copy, PartialEq, Eq, Debug)]
66pub(crate) enum TransactionState {
67    Write {
68        schema_did_change: bool,
69    },
70    Read,
71    /// PendingUpgrade remembers what transaction state was before upgrade to write (has_read_txn is true if before transaction were in Read state)
72    /// This is important, because if we failed to initialize write transaction immediatley - we need to end implicitly started read txn (e.g. for simiple INSERT INTO operation)
73    /// But for late upgrade of transaction we should keep read transaction active (e.g. BEGIN; SELECT ...; INSERT INTO ...)
74    PendingUpgrade {
75        has_read_txn: bool,
76    },
77    None,
78}
79
80pub(crate) struct TempDatabase {
81    pub(crate) db: Arc<Database>,
82    pub(crate) pager: Arc<Pager>,
83    #[cfg(not(target_family = "wasm"))]
84    _temp_dir: Option<TempDir>,
85}
86
87/// All of the connection-local state needed to manage the `TEMP` schema.
88///
89/// Grouped so that anything that touches the temp database (the pager,
90/// the last committed schema snapshot, the dirty-schema flag) lives in
91/// one place. The individual fields keep their own locks because their
92/// access patterns differ: `database` is read on every temp-qualified
93/// lookup while `committed_schema` is only touched at commit/rollback
94/// boundaries, and `schema_did_change` is flipped from inside `SetCookie`.
95pub(crate) struct TempDbContext {
96    /// Per-connection temp database (`TEMP_DB_ID`/schema `temp`).
97    /// Lazily initialized on first temp DDL.
98    pub(crate) database: RwLock<Option<TempDatabase>>,
99    /// Last committed snapshot of `database.read().as_ref().unwrap().db.schema`.
100    /// Updated on successful commit and consulted on full-txn rollback
101    /// to restore the in-memory temp schema (there is no shared
102    /// `Database::schema` for temp the way main has). `None` until the
103    /// first temp DDL is committed; a rollback with `None` resets the
104    /// temp schema to empty.
105    pub(crate) committed_schema: RwLock<Option<Arc<Schema>>>,
106    /// Set by `SetCookie` when a temp DDL runs; read by commit/rollback
107    /// to decide whether to snapshot/restore. Cleared on transaction
108    /// end. Mirrors the `schema_did_change` field inside
109    /// `TransactionState::Write` for the main DB.
110    pub(crate) schema_did_change: AtomicBool,
111}
112
113impl TempDbContext {
114    pub(crate) fn new() -> Self {
115        Self {
116            database: RwLock::new(None),
117            committed_schema: RwLock::new(None),
118            schema_did_change: AtomicBool::new(false),
119        }
120    }
121}
122
123#[derive(Debug, Clone)]
124pub(crate) struct NamedSavepointFrame {
125    pub(crate) name: String,
126    pub(crate) starts_transaction: bool,
127    pub(crate) deferred_fk_violations: isize,
128    /// Snapshot of `conn.schema` taken at SAVEPOINT begin. Used by
129    /// ROLLBACK TO to restore the in-memory main schema without re-
130    /// reading sqlite_schema from disk — disk reparse from inside a
131    /// vdbe opcode would block on cursor I/O (and additionally, for
132    /// sequences, on `prepare_internal + run_with_row_callback`),
133    /// violating the vdbe async contract. Cheap to capture: bumps
134    /// the `Arc<Schema>` refcount. DDL after SAVEPOINT goes through
135    /// `Connection::with_schema_mut` which uses `Arc::make_mut`, so
136    /// the snapshot keeps pointing to the pre-DDL schema even when
137    /// the current schema diverges.
138    pub(crate) main_schema_snapshot: Arc<Schema>,
139    /// Snapshot of `temp_db.db.schema` taken at SAVEPOINT begin. `None`
140    /// when the temp database had not been initialized yet. Used by
141    /// ROLLBACK TO to restore the in-memory temp schema after the
142    /// on-disk pages have been rolled back via the mirror call.
143    pub(crate) temp_schema_snapshot: Option<Arc<Schema>>,
144    /// Snapshot of the connection-local `database_schemas` map at
145    /// SAVEPOINT begin. Cheap — values are `Arc`. Used by ROLLBACK TO
146    /// to restore staged DDL on attached databases.
147    pub(crate) staged_schema_snapshot: HashMap<usize, Arc<Schema>>,
148}
149
150/// Info returned by `rollback_named_savepoint_frame` so callers can
151/// restore in-memory schema state after the pager has rolled back.
152pub(crate) struct RollbackFrameInfo {
153    pub(crate) main_schema_snapshot: Arc<Schema>,
154    pub(crate) temp_schema_snapshot: Option<Arc<Schema>>,
155    pub(crate) staged_schema_snapshot: HashMap<usize, Arc<Schema>>,
156}
157
158struct SchemaReparseGuard {
159    connection: Arc<Connection>,
160}
161
162impl Drop for SchemaReparseGuard {
163    fn drop(&mut self) {
164        self.connection
165            .schema_reparse_in_progress
166            .store(false, Ordering::SeqCst);
167    }
168}
169
170/// Re-entrant state for [`Connection::reparse_schema_nonblock`] and the
171/// VACUUM-only [`Connection::reparse_schema_with_cookie_keeping_sequences`].
172/// `Start` is the fresh state (cookie not yet read / schema build not yet
173/// begun); `Building` carries the half-built schema, captured table-valued
174/// functions, the held reparse guard, and the current sub-phase across IO
175/// yields.
176#[derive(Default)]
177pub enum ReparseSchemaState {
178    #[default]
179    Start,
180    Building(Box<ReparseSchemaInner>),
181}
182
183pub struct ReparseSchemaInner {
184    /// Held for the whole reparse so a concurrent reparse on this connection
185    /// trips the recursion assert. Dropped when the schema is finalized.
186    _guard: SchemaReparseGuard,
187    fresh: Schema,
188    /// Built-in table-valued functions captured from the old schema; rehydrated
189    /// after the sqlite_schema scan since they don't survive re-parsing.
190    tvfs: Vec<Arc<crate::vtab::VirtualTable>>,
191    /// VACUUM-supplied sequence descriptors to graft onto the rebuilt schema
192    /// instead of re-reading each backing table. `None` for a normal reparse,
193    /// which recovers descriptors from disk in the `PopulateSequences` phase.
194    preserved_sequences: Option<rustc_hash::FxHashMap<String, Arc<crate::schema::Sequence>>>,
195    phase: ReparsePhase,
196}
197
198enum ReparsePhase {
199    /// Scanning `SELECT * FROM sqlite_schema` into `fresh`.
200    ParseSchema {
201        parse: Box<crate::util::ParseSchemaRowsState>,
202    },
203    /// Recovering sequence descriptors from each `__turso_internal_seq_*`
204    /// backing table via SQL (or grafting the VACUUM-preserved map). The
205    /// per-backing-table descriptor read yields IO, so the worklist and the
206    /// in-flight statement are carried across re-entry here.
207    PopulateSequences {
208        /// `(backing_table_name, seq_name)` worklist; `None` until lazily
209        /// computed. Left empty when preserved sequences are grafted.
210        pending: Option<crate::alloc::Vec<(String, String)>>,
211        /// Index of the backing table currently being read.
212        idx: usize,
213        /// In-flight descriptor `SELECT`, created lazily per backing table.
214        stmt: Option<Box<Statement>>,
215        /// Descriptor row `(start, inc, min, max, cycle)` captured from `stmt`.
216        meta: Option<(i64, i64, i64, i64, bool)>,
217        /// Sequence reconstructed from `meta`, retained while the watermark
218        /// query yields IO.
219        seq: Option<crate::schema::Sequence>,
220        /// In-flight watermark `SELECT`, created after `seq` is known.
221        watermark_stmt: Option<Box<Statement>>,
222        /// Watermark row `(value, is_called)` captured from `watermark_stmt`.
223        watermark_row: Option<(i64, bool)>,
224    },
225    /// Loading custom type definitions from the internal types table.
226    LoadTypes {
227        stmt: Box<Statement>,
228        type_rows: Vec<String>,
229    },
230    /// Best-effort ANALYZE-stats refresh before finalizing.
231    RefreshStats {
232        stats: crate::stats::RefreshAnalyzeStatsState,
233    },
234}
235
236#[cfg(not(clt_turso_feature = "fs"))]
237#[derive(Default)]
238pub(crate) enum AttachDatabaseState {
239    #[default]
240    Start,
241}
242
243#[cfg(clt_turso_feature = "fs")]
244#[derive(Default)]
245pub(crate) enum AttachDatabaseState {
246    #[default]
247    Start,
248    Init(Box<AttachDatabaseInitState>),
249    Bootstrap(Box<AttachDatabaseBootstrapState>),
250    Publish {
251        alias: String,
252        db: Arc<Database>,
253        pager: Arc<Pager>,
254    },
255    Done,
256}
257
258#[cfg(clt_turso_feature = "fs")]
259pub(crate) struct AttachDatabaseInitState {
260    alias: String,
261    reserved_space: Option<u8>,
262    db: Arc<Database>,
263    attached_is_fresh: bool,
264    encryption_key: Option<EncryptionKey>,
265    init_st: crate::InitState,
266}
267
268#[cfg(clt_turso_feature = "fs")]
269pub(crate) struct AttachDatabaseBootstrapState {
270    alias: String,
271    db: Arc<Database>,
272    pager: Arc<Pager>,
273    encryption_key: Option<EncryptionKey>,
274    bootstrap_conn: Option<Arc<Connection>>,
275    bootstrap_st: crate::mvcc::database::BootstrapState,
276}
277
278/// Re-entrant driver state for
279/// [`Connection::load_sequence_descriptors_via_sql_nonblock`]. Walks every
280/// `__turso_internal_seq_*` backing table and registers its descriptor,
281/// carrying the worklist and the in-flight descriptor read across IO yields.
282#[derive(Default)]
283pub enum LoadSequenceDescriptorsState {
284    #[default]
285    Start,
286    Reading {
287        /// `(backing_table_name, seq_name)` worklist captured from the schema.
288        pending: crate::alloc::Vec<(String, String)>,
289        /// Index of the backing table currently being read.
290        idx: usize,
291        /// In-flight descriptor `SELECT`, created lazily per backing table.
292        stmt: Option<Box<Statement>>,
293        /// Descriptor row `(start, inc, min, max, cycle)` captured from `stmt`.
294        meta: Option<(i64, i64, i64, i64, bool)>,
295        /// Sequence reconstructed from `meta`, retained while the watermark
296        /// query yields IO.
297        seq: Option<crate::schema::Sequence>,
298        /// In-flight watermark `SELECT`, created after `seq` is known.
299        watermark_stmt: Option<Box<Statement>>,
300        /// Watermark row `(value, is_called)` captured from `watermark_stmt`.
301        watermark_row: Option<(i64, bool)>,
302    },
303}
304
305/// Re-entrant driver state for
306/// [`Connection::sync_autoincrement_backing_tables_from_sqlite_sequence_nonblock`].
307#[derive(Default)]
308pub enum SyncAutoincrementState {
309    #[default]
310    Start,
311    /// Reading all `(name, seq)` rows from `sqlite_sequence`.
312    ReadSeqRows {
313        stmt: Box<Statement>,
314        rows: Vec<(String, i64)>,
315    },
316    /// Per-table: read the backing `MAX(value)` then maybe upsert a watermark.
317    Process {
318        rows: Vec<(String, i64)>,
319        idx: usize,
320        sub: SyncRowStep,
321    },
322}
323
324/// Per-row sub-state of [`SyncAutoincrementState::Process`].
325#[derive(Default)]
326pub enum SyncRowStep {
327    /// Resolve `rows[idx]`'s backing table and start reading `MAX(value)`.
328    #[default]
329    Start,
330    /// Reading `MAX(value)` from the backing table.
331    ReadMax {
332        backing_table_name: String,
333        stmt: Box<Statement>,
334        current_max: Option<i64>,
335    },
336    /// Running the `INSERT OR REPLACE` watermark upsert.
337    Upsert { stmt: Box<Statement> },
338}
339
340/// Database connection handle.
341///
342/// If you add a setting that affects SQL compilation or execution, call
343/// `bump_prepare_context_generation()` in its setter so cached prepared
344/// statements know they need to be reprepared.
345pub struct Connection {
346    pub(crate) db: Arc<Database>,
347    pub(crate) pager: ArcSwap<Pager>,
348    pub(crate) schema: RwLock<Arc<Schema>>,
349    /// Per-database schema cache (database_index -> schema)
350    /// Loaded lazily to avoid copying all schemas on connection open
351    pub(super) database_schemas: RwLock<HashMap<usize, Arc<Schema>>>,
352    /// Whether to automatically commit transaction
353    pub(crate) auto_commit: AtomicBool,
354    pub(super) transaction_state: AtomicTransactionState,
355    /// True when an unfinished write statement inside an explicit transaction
356    /// was reset or dropped and there was no statement savepoint to undo only
357    /// that statement. COMMIT must roll back the whole transaction.
358    pub(crate) poisoned_tx: AtomicBool,
359    pub(super) last_insert_rowid: AtomicI64,
360    pub(crate) changes: AtomicI64,
361    pub(crate) total_changes: AtomicI64,
362    pub(crate) syms: parking_lot::RwLock<SymbolTable>,
363    pub(super) _shared_cache: bool,
364    pub(super) cache_size: AtomicI32,
365    /// page size used for an uninitialized database or the next vacuum command.
366    /// it's not always equal to the current page size of the database
367    pub(super) page_size: AtomicU16,
368    /// Allowed automatic WAL maintenance actions for this connection.
369    /// Stored as the `bits()` of a `WalAutoActions`. Default is
370    /// `WalAutoActions::all_enabled()`. `wal_auto_actions_disable` clears
371    /// every bit, opting out of both auto-checkpoint and WAL header
372    /// restart — sync-engine consumers rely on the latter staying disabled
373    /// because rotating the WAL header invalidates their published
374    /// watermarks.
375    pub(super) wal_auto_actions: AtomicU8,
376    /// Whether MVCC commits should include portable logical-change metadata in
377    /// the logical log.
378    ///
379    /// This is off by default because the metadata is only useful for raw-log
380    /// consumers such as sync clients.
381    #[cfg(clt_turso_feature = "conn_raw_api")]
382    pub(super) portable_logical_changes_enabled: AtomicBool,
383    #[cfg(clt_turso_feature = "conn_raw_api")]
384    pub(super) mvcc_log_metadata: RwLock<HashMap<String, String>>,
385    pub(super) capture_data_changes: RwLock<Option<CaptureDataChangesInfo>>,
386    /// CDC v2: transaction ID for grouping CDC records by transaction.
387    /// -1 means unset (will be assigned on first CDC write in the transaction).
388    pub(crate) cdc_transaction_id: AtomicI64,
389    pub(super) closed: AtomicBool,
390    /// Per-connection state for the `TEMP` schema (pager, last-committed
391    /// snapshot, dirty-schema flag). See `TempDbContext`.
392    pub(crate) temp: TempDbContext,
393    /// Attached databases
394    pub(super) attached_databases: RwLock<DatabaseCatalog>,
395    pub(super) query_only: AtomicBool,
396    pub(super) vdbe_trace: AtomicBool,
397    /// If enabled, the UPDATE/DELETE statements must have a WHERE clause
398    pub(super) dml_require_where: AtomicBool,
399    /// SQLite DQS misfeature: when ON (default), unresolved double-quoted identifiers
400    /// in DML statements fall back to string literals instead of raising an error.
401    pub(super) dqs_dml: AtomicBool,
402    /// Deprecated pragma: when ON, column names include table prefix (TABLE.COLUMN)
403    pub(super) full_column_names: AtomicBool,
404    /// Deprecated pragma: when ON (default), column refs use just the column name
405    pub(super) short_column_names: AtomicBool,
406    /// Per-connection runtime extension loading flag.
407    pub(super) enable_load_extension: AtomicBool,
408    /// Cumulative count of autonomous sequence inner-tx retries (each
409    /// `WriteWriteConflict` / `BusySnapshot` / `Conflict` that
410    /// `op_sequence_commit_inner_tx` absorbs via its retry budget bumps
411    /// this). Lives on the connection rather than `ProgramState` because
412    /// autocommit nextval/setval allocates a fresh `ProgramState` per
413    /// statement — the per-state counter resets on every Step and can't
414    /// witness across-statement contention. Tests use this counter to
415    /// assert the hot path is conflict-free: any non-zero increment on
416    /// a non-CYCLE nextval means inline backing-table compaction
417    /// (or another contended write) was reintroduced.
418    pub(crate) sequence_inner_retries: AtomicU64,
419    pub(crate) mv_tx: RwLock<Option<(crate::mvcc::database::TxID, TransactionMode)>>,
420    /// Per-attached-database MVCC transactions.
421    /// Main DB uses `mv_tx` above for zero-cost hot path access.
422    pub(crate) attached_mv_txs:
423        RwLock<HashMap<usize, (crate::mvcc::database::TxID, TransactionMode)>>,
424    #[cfg(any(clt_turso_tests, injected_yields))]
425    pub(super) yield_injector: RwLock<Option<Arc<dyn YieldInjector>>>,
426    #[cfg(any(clt_turso_tests, injected_yields))]
427    pub(super) failure_injector: RwLock<Option<Arc<dyn FailureInjector>>>,
428    #[cfg(any(clt_turso_tests, injected_yields))]
429    pub(super) yield_instance_id_counter: AtomicU64,
430
431    /// Per-connection view transaction states for uncommitted changes. This represents
432    /// one entry per view that was touched in the transaction.
433    pub(crate) view_transaction_states: AllViewsTxState,
434    /// Connection-level metrics aggregation
435    pub metrics: RwLock<ConnectionMetrics>,
436    /// Greater than zero if connection executes a program within a program
437    /// This is necessary in order for connection to not "finalize" transaction (commit/abort) when program ends
438    /// (because parent program is still pending and it will handle "finalization" instead)
439    ///
440    /// The state is integer as we may want to spawn deep nested programs (e.g. Root -[run]-> S1 -[run]-> S2 -[run]-> ...)
441    /// and we need to track current nestedness depth in order to properly understand when we will reach the root back again
442    pub(super) nestedness: AtomicI32,
443    /// Stack of currently compiling triggers to prevent recursive trigger subprogram compilation
444    pub(super) compiling_triggers: RwLock<Vec<Arc<Trigger>>>,
445    /// Stack of currently executing triggers to prevent recursive trigger execution
446    /// Only prevents the same trigger from firing again, allowing different triggers on the same table to fire
447    pub(super) executing_triggers: RwLock<Vec<Arc<Trigger>>>,
448    pub(crate) encryption_key: RwLock<Option<EncryptionKey>>,
449    pub(super) encryption_cipher_mode: AtomicCipherMode,
450    pub(super) sync_mode: AtomicSyncMode,
451    pub(super) temp_store: AtomicTempStore,
452    pub(super) data_sync_retry: AtomicBool,
453    /// Busy handler for lock contention
454    /// Default is BusyHandler::None (return SQLITE_BUSY immediately)
455    pub(super) busy_handler: RwLock<BusyHandler>,
456    /// Step-based progress callback for SQLite-compatible cancellation hooks.
457    pub(super) progress_handler: ProgressHandler,
458    /// Maximum execution time for a single statement on this connection.
459    /// `Duration::ZERO` means disabled.
460    pub(super) query_timeout_ms: AtomicU64,
461    /// True when sqlite3_interrupt()-style cancellation is pending for active root statements.
462    pub(super) interrupt_requested: AtomicBool,
463    /// Whether this is an internal connection used for MVCC bootstrap
464    pub(super) is_mvcc_bootstrap_connection: AtomicBool,
465    /// Whether pragma foreign_keys=ON for this connection
466    pub(super) fk_pragma: AtomicBool,
467    pub(crate) fk_deferred_violations: AtomicIsize,
468    /// Number of active top-level write statements on this connection.
469    ///
470    /// This is currently only 0 or 1. We return Busy instead of allowing a
471    /// second same-connection writer to start.
472    pub(crate) n_active_writes: AtomicI32,
473    /// Number of active root statements currently executing on this connection.
474    /// This is Turso's equivalent of SQLite's top-level active-VDBE count
475    /// (`db->nVdbeActive`) for user statements, excluding internal helpers and
476    /// subprogram execution.
477    pub(crate) n_active_root_statements: AtomicI32,
478    /// Whether pragma ignore_check_constraints=ON for this connection
479    pub(super) check_constraints_pragma: AtomicBool,
480    /// Track when each virtual table instance is currently in transaction.
481    pub(crate) vtab_txn_states: RwLock<HashSet<u64>>,
482    /// Connection-level named savepoint stack used to mirror savepoint state
483    /// onto temp/attached databases that start participating after SAVEPOINT.
484    pub(crate) named_savepoints: RwLock<Vec<NamedSavepointFrame>>,
485    /// True while this connection is rebuilding its schema from sqlite_schema.
486    /// Internal helper statements used during reload must not recursively
487    /// trigger another schema reparse on the same connection.
488    pub(crate) schema_reparse_in_progress: AtomicBool,
489    /// Generation counter bumped whenever any setting that affects PrepareContext
490    /// changes. Allows prepared statements to cheaply detect when they need to be
491    /// reprepared (single u64 comparison instead of rebuilding the full context).
492    /// IMPORTANT: this is a bit of a regression landmine because the generation
493    /// MUST be incremented whenever any setting that affects PrepareContext changes,
494    /// and this is not currently centralized; each setter bumps the generation individually.
495    pub(crate) prepare_context_generation: AtomicU64,
496    /// Per-connection last-returned value for each sequence (for currval()).
497    pub(crate) sequence_currvals: RwLock<HashMap<String, i64>>,
498}
499
500// SAFETY: This needs to be audited for thread safety.
501// See: https://github.com/tursodatabase/turso/issues/1552
502crate::assert::assert_send_sync!(Connection);
503
504impl Drop for Connection {
505    fn drop(&mut self) {
506        if !self.is_closed() {
507            // Roll back any active MVCC transactions so that MvStore entries
508            // don't leak and block future checkpoints.  The tx may have
509            // already been committed/aborted externally (e.g. by tests that
510            // manipulate MvStore directly), so only rollback if still active.
511            if let Some(mv_store) = self.db.get_mv_store().as_ref() {
512                if let Some(tx_id) = self.get_mv_tx_id() {
513                    let pager = self.pager.load();
514                    if mv_store.is_tx_rollbackable(tx_id) {
515                        mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID);
516                    } else {
517                        self.set_mv_tx(None);
518                    }
519                    pager.end_read_tx();
520                }
521            }
522            self.rollback_attached_mvcc_txs(false);
523
524            // Release any WAL locks the connection might be holding.
525            // This prevents deadlocks if a connection is dropped (e.g., due to a panic)
526            // while holding a read or write lock.
527            let pager = self.pager.load();
528            if let Some(wal) = &pager.wal {
529                if wal.holds_write_lock() {
530                    wal.end_write_tx();
531                }
532                if wal.holds_read_lock() {
533                    wal.end_read_tx();
534                }
535            }
536
537            // Also release WAL locks on all attached database pagers
538            self.with_all_attached_pagers_with_index(|attached_pagers| {
539                for (_, attached_pager) in attached_pagers {
540                    if let Some(wal) = &attached_pager.wal {
541                        if wal.holds_write_lock() {
542                            wal.end_write_tx();
543                        }
544                        if wal.holds_read_lock() {
545                            wal.end_read_tx();
546                        }
547                    }
548                }
549            });
550
551            // if connection wasn't properly closed, decrement the connection counter
552            self.db
553                .n_connections
554                .fetch_sub(1, crate::sync::atomic::Ordering::SeqCst);
555        }
556    }
557}
558
559impl Connection {
560    fn schema_reparse_guard(self: &Arc<Connection>) -> SchemaReparseGuard {
561        let was_reparsing = self.schema_reparse_in_progress.swap(true, Ordering::SeqCst);
562        turso_assert!(
563            !was_reparsing,
564            "schema reparse must not recurse on the same connection"
565        );
566        SchemaReparseGuard {
567            connection: self.clone(),
568        }
569    }
570
571    pub(crate) fn schema_reparse_in_progress(&self) -> bool {
572        self.schema_reparse_in_progress.load(Ordering::Acquire)
573    }
574
575    pub(crate) fn empty_temp_schema(&self) -> Arc<Schema> {
576        // with_options only fails if built-in type SQL is malformed (programmer bug).
577        let mut schema = Schema::with_options(self.db.experimental_custom_types_enabled())
578            .expect("built-in type definitions are malformed");
579        schema.generated_columns_enabled = self.db.experimental_generated_columns_enabled();
580        Arc::new(schema)
581    }
582
583    fn make_temp_database_opts(&self) -> DatabaseOpts {
584        DatabaseOpts::new()
585            .with_views(self.db.experimental_views_enabled())
586            .with_custom_types(self.db.experimental_custom_types_enabled())
587            .with_index_method(self.db.experimental_index_method_enabled())
588            .with_vacuum(self.db.experimental_vacuum_enabled())
589            .with_generated_columns(self.db.experimental_generated_columns_enabled())
590            .with_without_rowid(self.db.experimental_without_rowid_enabled())
591    }
592
593    fn effective_temp_store(&self) -> crate::TempStore {
594        let temp_store = self.get_temp_store();
595        #[cfg(clt_turso_feature = "fs")]
596        {
597            temp_store
598        }
599        #[cfg(not(clt_turso_feature = "fs"))]
600        {
601            let _ = temp_store;
602            crate::TempStore::Memory
603        }
604    }
605
606    #[cfg(clt_turso_feature = "fs")]
607    fn create_temp_database(&self) -> Result<TempDatabase> {
608        let temp_store = self.effective_temp_store();
609        let db_opts = self.make_temp_database_opts();
610        let page_size = self.get_page_size();
611
612        if matches!(temp_store, crate::TempStore::Memory) {
613            let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
614            let db = Database::open_file_with_flags(
615                io,
616                crate::util::MEMORY_PATH,
617                OpenFlags::Create,
618                db_opts,
619                None,
620            )?;
621            let pager = Arc::new(db._init(None)?);
622            pager.set_initial_page_size(page_size)?;
623            return Ok(TempDatabase {
624                db,
625                pager,
626                #[cfg(not(target_family = "wasm"))]
627                _temp_dir: None,
628            });
629        }
630
631        #[cfg(not(target_family = "wasm"))]
632        {
633            let temp_dir = self.create_tempdir()?;
634            let temp_path = temp_dir.path().join("tursodb-temp.db");
635            let temp_path_str = temp_path.to_str().ok_or_else(|| {
636                LimboError::InternalError("temp db path is not valid UTF-8".into())
637            })?;
638            // Always create a fresh IO for the temp file. Cloning the
639            // main db's IO is wrong when the main db uses a mock /
640            // simulated backend (e.g. the deterministic simulator
641            // with `--io-backend=memory`) that can't access real
642            // filesystem paths produced by `tempfile::tempdir()`.
643            let io = Database::io_for_path(temp_path_str)?;
644            let db = Database::open_file_with_flags(
645                io,
646                temp_path_str,
647                OpenFlags::Create,
648                db_opts,
649                None,
650            )?;
651            let pager = Arc::new(db._init(None)?);
652            pager.set_initial_page_size(page_size)?;
653            Ok(TempDatabase {
654                db,
655                pager,
656                _temp_dir: Some(temp_dir),
657            })
658        }
659
660        #[cfg(target_family = "wasm")]
661        {
662            let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
663            let db = Database::open_file_with_flags(
664                io,
665                crate::util::MEMORY_PATH,
666                OpenFlags::Create,
667                db_opts,
668                None,
669            )?;
670            let pager = Arc::new(db._init(None)?);
671            pager.set_initial_page_size(page_size)?;
672            Ok(TempDatabase { db, pager })
673        }
674    }
675
676    #[cfg(not(clt_turso_feature = "fs"))]
677    fn create_temp_database(&self) -> Result<TempDatabase> {
678        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
679        let db = Database::open_file_with_flags(
680            io,
681            crate::util::MEMORY_PATH,
682            OpenFlags::Create,
683            self.make_temp_database_opts(),
684            None,
685        )?;
686        let pager = Arc::new(db._init(None)?);
687        pager.set_initial_page_size(self.get_page_size())?;
688        Ok(TempDatabase {
689            db,
690            pager,
691            #[cfg(not(target_family = "wasm"))]
692            _temp_dir: None,
693        })
694    }
695
696    pub(crate) fn ensure_temp_database(&self) -> Result<()> {
697        if self.temp.database.read().is_some() {
698            return Ok(());
699        }
700
701        let temp_db = self.create_temp_database()?;
702        let mut guard = self.temp.database.write();
703        if guard.is_none() {
704            *guard = Some(temp_db);
705        }
706        Ok(())
707    }
708
709    /// Tear down the per-connection temp database.
710    ///
711    /// Drops the temp pager, clears the committed schema snapshot and
712    /// the dirty-schema flag. Called by `set_temp_store` when the user
713    /// changes `PRAGMA temp_store` outside of an explicit transaction.
714    fn reset_temp_database(&self) {
715        if let Some(temp_db) = self.temp.database.write().take() {
716            temp_db.pager.rollback_attached();
717        }
718        *self.temp.committed_schema.write() = None;
719        self.temp.schema_did_change.store(false, Ordering::Release);
720    }
721
722    /// Flag a temp-schema mutation within the current transaction so the
723    /// commit/rollback path knows to snapshot or restore the in-memory
724    /// temp schema. Called from `SetCookie` for `TEMP_DB_ID`.
725    pub(crate) fn mark_temp_schema_did_change(&self) {
726        // If we're marking the temp schema dirty, temp DDL must have
727        // just run against the temp pager — which means the temp
728        // database was initialized. The opposite state is unreachable.
729        turso_assert!(
730            self.temp.database.read().is_some(),
731            "mark_temp_schema_did_change called without an initialized temp database"
732        );
733        self.temp.schema_did_change.store(true, Ordering::Release);
734    }
735
736    /// On successful commit, snapshot the current `temp_db.db.schema`
737    /// into `committed_temp_schema` so a future full-txn rollback can
738    /// restore it. No-op if no temp DDL ran in this transaction.
739    pub(crate) fn commit_temp_schema(&self) {
740        if !self.temp.schema_did_change.load(Ordering::Acquire) {
741            return;
742        }
743        // `schema_did_change` is only ever set by
744        // `mark_temp_schema_did_change`, which asserts temp is
745        // initialized. If it's somehow clear here we have a logic
746        // bug — no safe recovery, so fail loud.
747        let guard = self.temp.database.read();
748        turso_assert!(
749            guard.is_some(),
750            "commit_temp_schema: schema_did_change set but temp is uninitialized"
751        );
752        let snap = guard
753            .as_ref()
754            .expect("asserted above")
755            .db
756            .schema
757            .lock()
758            .clone();
759        drop(guard);
760        // save snapshot for potential future rollback.
761        *self.temp.committed_schema.write() = Some(snap);
762        self.temp.schema_did_change.store(false, Ordering::Release);
763    }
764
765    /// On full-txn rollback, restore `temp_db.db.schema` from the last
766    /// committed snapshot. If nothing was ever committed, reset to an
767    /// empty schema (matches the disk state the pager rolled back to).
768    pub(crate) fn rollback_temp_schema(&self) {
769        if !self.temp.schema_did_change.load(Ordering::Acquire) {
770            return;
771        }
772        // Same invariant as `commit_temp_schema` — the flag can only
773        // be set while temp is initialized.
774        let committed = self.temp.committed_schema.read().clone();
775        {
776            let guard = self.temp.database.read();
777            turso_assert!(
778                guard.is_some(),
779                "rollback_temp_schema: schema_did_change set but temp is uninitialized"
780            );
781            let temp_db = guard.as_ref().expect("asserted above");
782            match committed {
783                Some(snap) => *temp_db.db.schema.lock() = snap,
784                None => *temp_db.db.schema.lock() = self.empty_temp_schema(),
785            }
786        }
787        self.temp.schema_did_change.store(false, Ordering::Release);
788        self.bump_prepare_context_generation();
789    }
790
791    /// Bump the prepare context generation counter. Must be called whenever any
792    /// connection setting that is tracked in `PrepareContext` changes, so that
793    /// prepared statements know they need to be reprepared.
794    #[inline]
795    pub(crate) fn bump_prepare_context_generation(&self) {
796        self.prepare_context_generation
797            .fetch_add(1, Ordering::Release);
798    }
799
800    #[inline]
801    pub(crate) fn prepare_context_generation(&self) -> u64 {
802        self.prepare_context_generation.load(Ordering::Acquire)
803    }
804
805    /// check if connection executes nested program (so it must not do any "finalization" work as parent program will handle it)
806    pub fn is_nested_stmt(&self) -> bool {
807        self.nestedness.load(Ordering::SeqCst) > 0
808    }
809    /// starts nested program execution
810    pub fn start_nested(&self) {
811        self.nestedness.fetch_add(1, Ordering::SeqCst);
812    }
813    /// ends nested program execution
814    pub fn end_nested(&self) {
815        self.nestedness.fetch_add(-1, Ordering::SeqCst);
816    }
817
818    /// Check if a specific trigger is currently compiling (for recursive trigger prevention)
819    pub fn trigger_is_compiling(&self, trigger: &Arc<Trigger>) -> bool {
820        let compiling = self.compiling_triggers.read();
821        if let Some(trigger) = compiling.iter().find(|t| Arc::ptr_eq(t, trigger)) {
822            tracing::debug!("Trigger is already compiling: {}", trigger.name);
823            return true;
824        }
825        false
826    }
827
828    pub fn start_trigger_compilation(&self, trigger: Arc<Trigger>) {
829        tracing::debug!("Starting trigger compilation: {}", trigger.name);
830        self.compiling_triggers.write().push(trigger);
831    }
832
833    pub fn end_trigger_compilation(&self) {
834        tracing::debug!(
835            "Ending trigger compilation: {:?}",
836            self.compiling_triggers.read().last().map(|t| &t.name)
837        );
838        self.compiling_triggers.write().pop();
839    }
840
841    /// Check if a specific trigger is currently executing (for recursive trigger prevention)
842    pub fn is_trigger_executing(&self, trigger: &Arc<Trigger>) -> bool {
843        let executing = self.executing_triggers.read();
844        if let Some(active_trigger) = executing.iter().find(|t| Arc::ptr_eq(t, trigger)) {
845            tracing::debug!("Trigger is already executing: {}", trigger.name);
846            debug_assert!(Arc::ptr_eq(active_trigger, trigger));
847            return true;
848        }
849        false
850    }
851
852    pub fn start_trigger_execution(&self, trigger: Arc<Trigger>) {
853        tracing::debug!("Starting trigger execution: {}", trigger.name);
854        self.executing_triggers.write().push(trigger);
855    }
856
857    pub fn end_trigger_execution(&self) {
858        tracing::debug!(
859            "Ending trigger execution: {:?}",
860            self.executing_triggers.read().last().map(|t| &t.name)
861        );
862        self.executing_triggers.write().pop();
863    }
864
865    fn should_retry_cross_process_schema_lookup(
866        self: &Arc<Connection>,
867        err: &LimboError,
868    ) -> Result<bool> {
869        let LimboError::ParseError(msg) = err else {
870            return Ok(false);
871        };
872        if !msg.contains("no such table") && !msg.contains("table not found") {
873            return Ok(false);
874        }
875        if self.get_tx_state() != TransactionState::None {
876            return Ok(false);
877        }
878        if self.db.shared_wal_coordination()?.is_none() {
879            return Ok(false);
880        }
881        self.maybe_reparse_schema()?;
882        Ok(true)
883    }
884
885    #[turso_macros::trace_stack]
886    fn compile_cmd(
887        self: &Arc<Connection>,
888        cmd: Cmd,
889        input: &str,
890    ) -> Result<(Program, Arc<Pager>, QueryMode)> {
891        self.maybe_update_schema();
892
893        let syms = self.syms.read();
894        let pager = self.pager.load().clone();
895        let mode = QueryMode::new(&cmd);
896        let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd;
897        let schema = self.schema.read().clone();
898        match translate::translate(
899            &schema,
900            stmt,
901            pager.clone(),
902            self.clone(),
903            &syms,
904            mode,
905            input,
906        ) {
907            Ok(program) => Ok((program, pager, mode)),
908            Err(err) if self.should_retry_cross_process_schema_lookup(&err)? => {
909                // Cold path: re-parse the SQL from scratch after schema refresh rather
910                // than cloning the original AST, which can overflow the stack
911                // on deeply nested expression trees.
912                drop(syms);
913                let cmd = {
914                    crate::stack::trace_stack!("schema_retry_parse");
915                    let mut parser = Parser::new(input.as_bytes());
916                    let Some(cmd) = parser.next_cmd()? else {
917                        return Err(err);
918                    };
919                    cmd
920                };
921                self.maybe_update_schema();
922                let syms = self.syms.read();
923                let pager = self.pager.load().clone();
924                let mode = QueryMode::new(&cmd);
925                let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd;
926                let schema = self.schema.read().clone();
927                translate::translate(
928                    &schema,
929                    stmt,
930                    pager.clone(),
931                    self.clone(),
932                    &syms,
933                    mode,
934                    input,
935                )
936                .map(|program| (program, pager, mode))
937            }
938            Err(err) => Err(err),
939        }
940    }
941
942    pub fn prepare(self: &Arc<Connection>, sql: impl AsRef<str>) -> Result<Statement> {
943        self._prepare(sql)
944    }
945
946    pub(crate) fn prepare_internal(
947        self: &Arc<Connection>,
948        sql: impl AsRef<str>,
949    ) -> Result<Statement> {
950        self.prepare_with_origin(sql, StatementOrigin::InternalHelper)
951    }
952
953    #[instrument(skip_all, level = Level::DEBUG)]
954    pub fn _prepare(self: &Arc<Connection>, sql: impl AsRef<str>) -> Result<Statement> {
955        self.prepare_with_origin(sql, StatementOrigin::Root)
956    }
957
958    #[turso_macros::trace_stack]
959    fn prepare_with_origin(
960        self: &Arc<Connection>,
961        sql: impl AsRef<str>,
962        origin: StatementOrigin,
963    ) -> Result<Statement> {
964        if self.is_closed() {
965            return Err(LimboError::InternalError("Connection closed".to_string()));
966        }
967        if sql.as_ref().is_empty() {
968            return Err(LimboError::InvalidArgument(
969                "The supplied SQL string contains no statements".to_string(),
970            ));
971        }
972
973        let needs_nested_guard = origin.needs_nested_guard();
974        if needs_nested_guard {
975            self.start_nested();
976        }
977        let result = (|| {
978            let sql = sql.as_ref();
979            tracing::debug!("Preparing: {}", sql);
980            let (cmd, byte_offset_end) = {
981                crate::stack::trace_stack!("parse");
982                let mut parser = Parser::new(sql.as_bytes());
983                let cmd = match parser.next_cmd()? {
984                    Some(cmd) => cmd,
985                    None => {
986                        return Err(LimboError::InvalidArgument(
987                            "The supplied SQL string contains no statements".to_string(),
988                        ));
989                    }
990                };
991                (cmd, parser.offset())
992            };
993            let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
994                .unwrap()
995                .trim();
996            let (program, pager, mode) = self.compile_cmd(cmd, input)?;
997
998            Ok(Statement::new_with_origin(
999                program,
1000                pager,
1001                mode,
1002                byte_offset_end,
1003                origin,
1004                needs_nested_guard,
1005            ))
1006        })();
1007        if result.is_err() && needs_nested_guard {
1008            self.end_nested();
1009        }
1010        result
1011    }
1012
1013    /// Prepare a statement from an AST node directly, skipping SQL parsing.
1014    /// This is more efficient when AST is already available or constructed programmatically.
1015    pub fn prepare_stmt(self: &Arc<Connection>, stmt: ast::Stmt) -> Result<Statement> {
1016        self.prepare_stmt_with_origin(stmt, StatementOrigin::Root)
1017    }
1018
1019    #[turso_macros::trace_stack]
1020    fn prepare_stmt_with_origin(
1021        self: &Arc<Connection>,
1022        stmt: ast::Stmt,
1023        origin: StatementOrigin,
1024    ) -> Result<Statement> {
1025        if self.is_closed() {
1026            return Err(LimboError::InternalError("Connection closed".to_string()));
1027        }
1028        let needs_nested_guard = origin.needs_nested_guard();
1029        if needs_nested_guard {
1030            self.start_nested();
1031        }
1032        let result = (|| {
1033            self.maybe_update_schema();
1034            let syms = self.syms.read();
1035            let pager = self.pager.load().clone();
1036            let mode = QueryMode::Normal;
1037            let schema = self.schema.read().clone();
1038            let program = translate::translate(
1039                &schema,
1040                stmt,
1041                pager.clone(),
1042                self.clone(),
1043                &syms,
1044                mode,
1045                "<ast>", // No SQL input string available
1046            )?;
1047            Ok(Statement::new_with_origin(
1048                program,
1049                pager,
1050                mode,
1051                0,
1052                origin,
1053                needs_nested_guard,
1054            ))
1055        })();
1056        if result.is_err() && needs_nested_guard {
1057            self.end_nested();
1058        }
1059        result
1060    }
1061
1062    /// Whether this is an internal connection used for MVCC bootstrap
1063    pub fn is_mvcc_bootstrap_connection(&self) -> bool {
1064        self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst)
1065    }
1066
1067    /// Promote MVCC bootstrap connection to a regular connection so it reads from the MV store again.
1068    pub fn promote_to_regular_connection(&self) {
1069        assert!(self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst));
1070        self.is_mvcc_bootstrap_connection
1071            .store(false, Ordering::SeqCst);
1072    }
1073
1074    /// Demote regular connection to MVCC bootstrap connection so it does not read from the MV store.
1075    pub fn demote_to_mvcc_connection(&self) {
1076        assert!(!self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst));
1077        self.is_mvcc_bootstrap_connection
1078            .store(true, Ordering::SeqCst);
1079    }
1080
1081    /// Parse schema from scratch if version of schema for the connection differs from the schema cookie in the root page.
1082    /// This function must be called outside of any transaction because internally it will start transaction session by itself.
1083    /// In multi-process mode, this is the only way to discover schema changes made by other processes.
1084    pub fn maybe_reparse_schema(self: &Arc<Connection>) -> Result<()> {
1085        let pager = self.pager.load().clone();
1086        let mv_store = self.mv_store();
1087
1088        // maybe_reparse_schema must be called outside any explicit transaction
1089        // because it starts its own read transaction to load a fresh view of
1090        // sqlite_schema from disk.
1091        if self.get_tx_state() != TransactionState::None {
1092            return Ok(());
1093        }
1094        let had_main_mv_tx = self.get_mv_tx().is_some();
1095
1096        if self.db.shared_wal_coordination()?.is_some() {
1097            // Cross-process schema changes can leave page 1 and sqlite_schema
1098            // pages cached from an earlier WAL snapshot. Drop the cache before
1099            // probing the cookie so reparsing observes the current committed view.
1100            pager.clear_page_cache(false);
1101            pager.set_schema_cookie(None);
1102        }
1103
1104        let on_disk_schema_version = if mv_store.as_ref().is_some() {
1105            self.read_current_schema_cookie().or_else(|err| match err {
1106                LimboError::Page1NotAlloc => Ok(0),
1107                other => Err(other),
1108            })?
1109        } else {
1110            // first, quickly read schema_version from the root page in order to check if schema changed
1111            pager.begin_read_tx()?;
1112            let on_disk_schema_version = pager
1113                .io
1114                .block(|| pager.with_header(|header| header.schema_cookie));
1115
1116            let on_disk_schema_version = match on_disk_schema_version {
1117                Ok(db_schema_version) => db_schema_version.get(),
1118                Err(LimboError::Page1NotAlloc) => {
1119                    // this means this is a fresh db, so return a schema version of 0
1120                    0
1121                }
1122                Err(err) => {
1123                    pager.end_read_tx();
1124                    return Err(err);
1125                }
1126            };
1127            pager.end_read_tx();
1128            on_disk_schema_version
1129        };
1130
1131        let db_schema_version = self.db.schema.lock().schema_version;
1132        tracing::debug!(
1133            "path: {}, db_schema_version={} vs on_disk_schema_version={}",
1134            self.db.path,
1135            db_schema_version,
1136            on_disk_schema_version
1137        );
1138        // if schema_versions matches - exit early
1139        if db_schema_version == on_disk_schema_version {
1140            return Ok(());
1141        }
1142
1143        // start read transaction manually, because we will read schema cookie once again and
1144        // we must be sure that it will consistent with schema content
1145        //
1146        // from now on we must be very careful with errors propagation
1147        // in order to not accidentally keep read transaction opened
1148        pager.begin_read_tx()?;
1149        self.set_tx_state(TransactionState::Read);
1150
1151        let reparse_result = self.reparse_schema();
1152
1153        let previous = self.transaction_state.swap(TransactionState::None);
1154        turso_assert!(
1155            matches!(previous, TransactionState::None | TransactionState::Read),
1156            "unexpected end transaction state"
1157        );
1158        // close opened transaction if it was kept open
1159        // (in most cases, it will be automatically closed if stmt was executed properly)
1160        if previous == TransactionState::Read {
1161            pager.end_read_tx();
1162        }
1163        if !had_main_mv_tx {
1164            self.clear_internal_main_mvcc_tx(&pager);
1165        }
1166
1167        reparse_result?;
1168
1169        let schema = self.schema.read().clone();
1170        self.db.update_schema_if_newer(schema);
1171        Ok(())
1172    }
1173
1174    /// Parse schema from scratch even if the schema cookie did not change.
1175    ///
1176    /// Sync replace-base can install a page snapshot outside ordinary SQL DDL.
1177    /// The replacement may reuse the same schema cookie while changing root
1178    /// pages, so cookie-based refresh would keep stale btree metadata.
1179    #[cfg(clt_turso_feature = "conn_raw_api")]
1180    pub fn force_reparse_schema(self: &Arc<Connection>) -> Result<()> {
1181        self.force_reparse_schema_inner(true)
1182    }
1183
1184    /// Like [`Self::force_reparse_schema`], but refreshes only this connection's
1185    /// own schema snapshot without publishing it to the shared database cache.
1186    ///
1187    /// Use this when the caller must further mutate the schema before it becomes
1188    /// visible to other connections.
1189    pub fn force_reparse_schema_without_publish(self: &Arc<Connection>) -> Result<()> {
1190        self.force_reparse_schema_inner(false)
1191    }
1192
1193    fn force_reparse_schema_inner(self: &Arc<Connection>, publish: bool) -> Result<()> {
1194        if self.get_tx_state() != TransactionState::None {
1195            return Err(LimboError::Busy);
1196        }
1197        if self.get_mv_tx().is_some() || self.next_attached_mv_tx().is_some() {
1198            return Err(LimboError::Busy);
1199        }
1200
1201        let pager = self.pager.load().clone();
1202        pager.clear_page_cache(false);
1203        pager.set_schema_cookie(None);
1204        pager.begin_read_tx()?;
1205        self.set_tx_state(TransactionState::Read);
1206
1207        let reparse_result = self.reparse_schema();
1208
1209        let previous = self.transaction_state.swap(TransactionState::None);
1210        turso_assert!(
1211            matches!(previous, TransactionState::None | TransactionState::Read),
1212            "unexpected end transaction state"
1213        );
1214        if previous == TransactionState::Read {
1215            pager.end_read_tx();
1216        }
1217        self.clear_internal_main_mvcc_tx(&pager);
1218
1219        reparse_result?;
1220
1221        if publish {
1222            let schema = self.schema.read().clone();
1223            self.db.update_schema_if_newer(schema);
1224        }
1225        Ok(())
1226    }
1227
1228    fn clear_internal_main_mvcc_tx(&self, pager: &Arc<Pager>) {
1229        let Some(tx_id) = self.get_mv_tx_id() else {
1230            return;
1231        };
1232        if let Some(mv_store) = self.mv_store().as_ref() {
1233            if mv_store.is_tx_rollbackable(tx_id) {
1234                mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID);
1235            } else {
1236                self.set_mv_tx(None);
1237            }
1238        } else {
1239            self.set_mv_tx(None);
1240        }
1241        pager.cleanup_read_tx();
1242    }
1243
1244    /// Blocking shim: drives [`Self::reparse_schema_nonblock`] to completion.
1245    /// Retained for the many synchronous callers (statement reprepare, attach,
1246    /// extension load, vacuum). The genuinely non-blocking callers (MVCC
1247    /// bootstrap, the open-db state machine) drive `*_nonblock` directly.
1248    pub(crate) fn reparse_schema(self: &Arc<Connection>) -> Result<()> {
1249        let io = self.pager.load().io.clone();
1250        let mut state = ReparseSchemaState::default();
1251        io.block(|| self.reparse_schema_nonblock(&mut state))
1252    }
1253
1254    /// VACUUM-only reparse that grafts a caller-supplied sequence-
1255    /// descriptor map onto the freshly parsed schema rather than re-
1256    /// reading the backing tables from disk. VACUUM preserves every
1257    /// sequence's definition (start/inc/min/max/cycle) — only physical
1258    /// page locations change — so the source connection's pre-VACUUM
1259    /// sequences map is still valid for the post-VACUUM image.
1260    ///
1261    /// Blocking shim: VACUUM runs through the VDBE stepping layer (which
1262    /// does not thread IO out), so it drives the non-blocking reparse to
1263    /// completion here, seeding the `PopulateSequences` phase with the
1264    /// preserved map so it grafts rather than re-reading each backing table.
1265    pub(crate) fn reparse_schema_with_cookie_keeping_sequences(
1266        self: &Arc<Connection>,
1267        cookie: u32,
1268        sequences: rustc_hash::FxHashMap<String, Arc<crate::schema::Sequence>>,
1269    ) -> Result<()> {
1270        let io = self.pager.load().io.clone();
1271        let mut state = ReparseSchemaState::default();
1272        // `init_reparse_building` consumes the preserved map exactly once, on
1273        // the first (Start) invocation; `io.block` re-invokes the closure on
1274        // every IO completion, so `take()` yields `Some` only that first time.
1275        let mut preserved = Some(sequences);
1276        io.block(|| {
1277            if matches!(state, ReparseSchemaState::Start) {
1278                state = ReparseSchemaState::Building(Box::new(
1279                    self.init_reparse_building(cookie, preserved.take())?,
1280                ));
1281            }
1282            self.drive_reparse_building(&mut state)
1283        })
1284    }
1285
1286    /// Non-blocking schema reparse. Reads the current schema cookie, then drives
1287    /// the schema rebuild via the shared [`ReparseSchemaState`].
1288    pub(crate) fn reparse_schema_nonblock(
1289        self: &Arc<Connection>,
1290        state: &mut ReparseSchemaState,
1291    ) -> Result<crate::types::IOResult<()>> {
1292        use crate::types::IOResult;
1293        if matches!(state, ReparseSchemaState::Start) {
1294            // read cookie before consuming statement program - otherwise we can
1295            // end up reading cookie with closed transaction state
1296            let cookie = crate::return_if_io!(self.read_current_schema_cookie_nonblock());
1297            *state =
1298                ReparseSchemaState::Building(Box::new(self.init_reparse_building(cookie, None)?));
1299        }
1300        self.drive_reparse_building(state)
1301    }
1302
1303    /// Synchronous setup shared by the reparse entry points: install the cookie,
1304    /// build a fresh schema, capture table-valued functions, install the empty
1305    /// schema (the reprepare hack), and prepare the sqlite_schema scan.
1306    /// `preserved_sequences` is `Some` only for VACUUM, which grafts the map in
1307    /// the `PopulateSequences` phase instead of re-reading the backing tables.
1308    fn init_reparse_building(
1309        self: &Arc<Connection>,
1310        cookie: u32,
1311        preserved_sequences: Option<rustc_hash::FxHashMap<String, Arc<crate::schema::Sequence>>>,
1312    ) -> Result<ReparseSchemaInner> {
1313        let guard = self.schema_reparse_guard();
1314        self.pager.load().set_schema_cookie(Some(cookie));
1315        // create fresh schema as some objects can be deleted
1316        let mut fresh = Schema::with_options(self.experimental_custom_types_enabled())?;
1317        fresh.generated_columns_enabled = self.db.experimental_generated_columns_enabled();
1318        fresh.schema_version = cookie;
1319
1320        // Capture built-in table-valued functions (e.g. generate_series, json_each)
1321        // before dropping the old schema. These are registered programmatically and
1322        // don't survive re-parsing from sqlite_schema alone.
1323        let tvfs: Vec<Arc<crate::vtab::VirtualTable>> = self
1324            .schema
1325            .read()
1326            .tables
1327            .values()
1328            .filter_map(|table| match table.as_ref() {
1329                crate::schema::Table::Virtual(vtab)
1330                    if matches!(vtab.kind, turso_ext::VTabKind::TableValuedFunction) =>
1331                {
1332                    Some(vtab.clone())
1333                }
1334                _ => None,
1335            })
1336            .collect();
1337
1338        // TODO: this is hack to avoid a cyclical problem with schema reprepare
1339        // The problem here is that we prepare a statement here, but when the statement tries
1340        // to execute it, it first checks the schema cookie to see if it needs to reprepare the statement.
1341        // But in this occasion it will always reprepare, and we get an error. So we trick the statement by swapping our schema
1342        // with a new clean schema that has the same header cookie.
1343        self.with_schema_mut(|schema| {
1344            *schema = fresh.try_clone()?;
1345            Ok::<_, crate::alloc::TryReserveError>(())
1346        })??;
1347
1348        let stmt = self.prepare("SELECT * FROM sqlite_schema")?;
1349
1350        // MVCC bootstrap connection gets the "baseline" from the DB file and ignores anything in MV store
1351        let mv_tx = if self.is_mvcc_bootstrap_connection() {
1352            None
1353        } else {
1354            self.get_mv_tx()
1355        };
1356        Ok(ReparseSchemaInner {
1357            _guard: guard,
1358            fresh,
1359            tvfs,
1360            preserved_sequences,
1361            phase: ReparsePhase::ParseSchema {
1362                parse: Box::new(crate::util::ParseSchemaRowsState::new(stmt, mv_tx)),
1363            },
1364        })
1365    }
1366
1367    /// Drive the schema-rebuild phases held in `state` (must be `Building`).
1368    /// Yields IO; on completion installs the finished schema and resets `state`.
1369    fn drive_reparse_building(
1370        self: &Arc<Connection>,
1371        state: &mut ReparseSchemaState,
1372    ) -> Result<crate::types::IOResult<()>> {
1373        use crate::types::IOResult;
1374        loop {
1375            let ReparseSchemaState::Building(inner) = state else {
1376                unreachable!("drive_reparse_building requires Building state");
1377            };
1378            match &mut inner.phase {
1379                ReparsePhase::ParseSchema { parse } => {
1380                    // Resolver so attached-db qualifiers in temp triggers can be
1381                    // mapped to their actual index on this connection.
1382                    let attached_resolver = |name: &str| -> Option<usize> {
1383                        self.attached_databases
1384                            .read()
1385                            .get_database_by_name(&crate::util::normalize_ident(name))
1386                            .map(|(idx, _)| idx)
1387                    };
1388                    crate::return_if_io!(crate::util::parse_schema_rows(
1389                        parse,
1390                        &mut inner.fresh,
1391                        &self.syms.read(),
1392                        &attached_resolver,
1393                    ));
1394
1395                    // Rehydrate built-in table-valued functions captured at init.
1396                    for vtab in &inner.tvfs {
1397                        let normalized = crate::util::normalize_ident(&vtab.name);
1398                        inner.fresh.tables.entry(normalized).or_insert_with(|| {
1399                            Arc::new(crate::schema::Table::Virtual(vtab.clone()))
1400                        });
1401                    }
1402
1403                    // Next: recover sequence descriptors (or graft the VACUUM map).
1404                    inner.phase = ReparsePhase::PopulateSequences {
1405                        pending: None,
1406                        idx: 0,
1407                        stmt: None,
1408                        meta: None,
1409                        seq: None,
1410                        watermark_stmt: None,
1411                        watermark_row: None,
1412                    };
1413                }
1414                ReparsePhase::PopulateSequences {
1415                    pending,
1416                    idx,
1417                    stmt,
1418                    meta,
1419                    seq,
1420                    watermark_stmt,
1421                    watermark_row,
1422                } => {
1423                    // Lazy init: graft the VACUUM-preserved descriptor map, or
1424                    // compute the worklist of backing tables to read from disk.
1425                    // When there is real work, install `fresh` first so the
1426                    // descriptor SELECTs can resolve the backing tables.
1427                    if pending.is_none() {
1428                        if let Some(sequences) = inner.preserved_sequences.take() {
1429                            inner.fresh.sequences = sequences;
1430                            *pending = Some(crate::alloc::vec![]);
1431                        } else {
1432                            let work = inner.fresh.sequence_backing_table_names();
1433                            if !work.is_empty() {
1434                                self.with_schema_mut(|schema| {
1435                                    *schema = inner.fresh.try_clone()?;
1436                                    Ok::<_, crate::alloc::TryReserveError>(())
1437                                })??;
1438                            }
1439                            *pending = Some(work);
1440                        }
1441                    }
1442
1443                    // Read each remaining backing table's descriptor row. The
1444                    // backing table is internal; a missing/unreadable descriptor
1445                    // row is on-disk corruption (not "sequence missing"), so any
1446                    // failure surfaces as `Corrupt` and fails the open rather than
1447                    // silently dropping the sequence.
1448                    loop {
1449                        let entry = {
1450                            let work = pending.as_ref().expect("pending initialized above");
1451                            if *idx >= work.len() {
1452                                break;
1453                            }
1454                            work[*idx].clone()
1455                        };
1456                        let (backing_table_name, seq_name) = entry;
1457                        let normalized = crate::util::normalize_ident(&seq_name);
1458                        if inner.fresh.sequences.contains_key(&normalized) {
1459                            *idx += 1;
1460                            *stmt = None;
1461                            *meta = None;
1462                            *seq = None;
1463                            *watermark_stmt = None;
1464                            *watermark_row = None;
1465                            continue;
1466                        }
1467                        if seq.is_none() {
1468                            crate::return_if_io!(self.read_seq_descriptor_row_nonblock(
1469                                &backing_table_name,
1470                                &seq_name,
1471                                stmt,
1472                                meta,
1473                            ));
1474                            *seq = Some(Self::sequence_from_descriptor_meta(
1475                                &seq_name,
1476                                &backing_table_name,
1477                                *meta,
1478                            )?);
1479                            *stmt = None;
1480                            *meta = None;
1481                        }
1482                        let sequence = seq.as_ref().expect("sequence set above");
1483                        crate::return_if_io!(self.read_sequence_watermark_row_nonblock(
1484                            &backing_table_name,
1485                            sequence,
1486                            watermark_stmt,
1487                            watermark_row,
1488                        ));
1489                        let watermark = Self::sequence_watermark_from_row(
1490                            &backing_table_name,
1491                            sequence,
1492                            *watermark_row,
1493                        )?;
1494                        if let Some(mv_store) = self.db.get_mv_store().as_ref() {
1495                            mv_store.set_sequence_watermark(&normalized, watermark);
1496                        }
1497                        let sequence = seq.take().expect("sequence set above");
1498                        inner.fresh.sequences.insert(normalized, Arc::new(sequence));
1499                        *idx += 1;
1500                        *stmt = None;
1501                        *meta = None;
1502                        *watermark_stmt = None;
1503                        *watermark_row = None;
1504                    }
1505
1506                    // Decide whether to load custom types next.
1507                    if self.experimental_custom_types_enabled()
1508                        && inner
1509                            .fresh
1510                            .tables
1511                            .contains_key(crate::schema::TURSO_TYPES_TABLE_NAME)
1512                    {
1513                        // Temporarily install the schema so we can query against it.
1514                        self.with_schema_mut(|schema| {
1515                            *schema = inner.fresh.try_clone()?;
1516                            Ok::<_, crate::alloc::TryReserveError>(())
1517                        })??;
1518                        let stmt = self.prepare_internal(format!(
1519                            "SELECT name, sql FROM {}",
1520                            crate::schema::TURSO_TYPES_TABLE_NAME
1521                        ))?;
1522                        inner.phase = ReparsePhase::LoadTypes {
1523                            stmt: Box::new(stmt),
1524                            type_rows: Vec::new(),
1525                        };
1526                    } else {
1527                        inner.phase = ReparsePhase::RefreshStats {
1528                            stats: Default::default(),
1529                        };
1530                    }
1531                }
1532                ReparsePhase::LoadTypes { stmt, type_rows } => {
1533                    // Type loading is best-effort: log and continue on error.
1534                    let scan = (|| -> Result<IOResult<()>> {
1535                        crate::return_if_io!(stmt.run_with_row_callback_nonblock(|row| {
1536                            type_rows.push(row.get::<&str>(1)?.to_string());
1537                            Ok(())
1538                        }));
1539                        Ok(IOResult::Done(()))
1540                    })();
1541                    match scan {
1542                        Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
1543                        Ok(IOResult::Done(())) => {
1544                            let type_rows = std::mem::take(type_rows);
1545                            if let Err(e) = inner.fresh.load_type_definitions(&type_rows) {
1546                                tracing::warn!("Failed to load custom types: {}", e);
1547                            }
1548                            inner.phase = ReparsePhase::RefreshStats {
1549                                stats: Default::default(),
1550                            };
1551                        }
1552                        Err(e) => {
1553                            tracing::warn!("Failed to load custom types: {}", e);
1554                            inner.phase = ReparsePhase::RefreshStats {
1555                                stats: Default::default(),
1556                            };
1557                        }
1558                    }
1559                }
1560                ReparsePhase::RefreshStats { stats } => {
1561                    // Best-effort load stats if sqlite_stat1 is present.
1562                    crate::return_if_io!(crate::stats::refresh_analyze_stats_nonblock(self, stats));
1563
1564                    // Finalize: install the rebuilt schema. Take ownership so the
1565                    // guard drops and `state` is reusable.
1566                    let ReparseSchemaState::Building(inner) = std::mem::take(state) else {
1567                        unreachable!("state is Building");
1568                    };
1569                    let fresh = inner.fresh;
1570                    tracing::debug!(
1571                        "reparse_schema: schema_version={}, tables={:?}",
1572                        fresh.schema_version,
1573                        fresh.tables.keys()
1574                    );
1575                    self.with_schema_mut(|schema| {
1576                        *schema = fresh;
1577                    })?;
1578                    return Ok(IOResult::Done(()));
1579                }
1580            }
1581        }
1582    }
1583
1584    pub(crate) fn read_current_schema_cookie(&self) -> Result<u32> {
1585        if let Some(mv_store) = self.mv_store().as_ref() {
1586            let tx_id = self.get_mv_tx_id();
1587            mv_store.with_header(|header| header.schema_cookie.get(), tx_id.as_ref())
1588        } else {
1589            let pager = self.pager.load();
1590            pager
1591                .io
1592                .block(|| pager.with_header(|header| header.schema_cookie))
1593                .map(|cookie| cookie.get())
1594        }
1595    }
1596
1597    /// Non-blocking variant of [`Self::read_current_schema_cookie`]. The MVCC
1598    /// path reads an in-memory header (never yields); the pager path may yield
1599    /// while reading page 1. Idempotent across re-entry.
1600    pub(crate) fn read_current_schema_cookie_nonblock(
1601        &self,
1602    ) -> Result<crate::types::IOResult<u32>> {
1603        use crate::types::IOResult;
1604        if let Some(mv_store) = self.mv_store().as_ref() {
1605            let tx_id = self.get_mv_tx_id();
1606            let cookie =
1607                mv_store.with_header(|header| header.schema_cookie.get(), tx_id.as_ref())?;
1608            Ok(crate::types::IOResult::Done(cookie))
1609        } else {
1610            let pager = self.pager.load();
1611            let cookie = crate::return_if_io!(pager.with_header(|header| header.schema_cookie));
1612            Ok(crate::types::IOResult::Done(cookie.get()))
1613        }
1614    }
1615
1616    #[instrument(skip_all, level = Level::DEBUG)]
1617    pub fn prepare_execute_batch(self: &Arc<Connection>, sql: impl AsRef<str>) -> Result<()> {
1618        if self.is_closed() {
1619            return Err(LimboError::InternalError("Connection closed".to_string()));
1620        }
1621        if sql.as_ref().is_empty() {
1622            return Err(LimboError::InvalidArgument(
1623                "The supplied SQL string contains no statements".to_string(),
1624            ));
1625        }
1626        let sql = sql.as_ref();
1627        tracing::trace!("Preparing and executing batch: {}", sql);
1628        let mut parser = Parser::new(sql.as_bytes());
1629        while let Some(cmd) = parser.next_cmd()? {
1630            let byte_offset_end = parser.offset();
1631            let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
1632                .unwrap()
1633                .trim();
1634            let (program, pager, mode) = self.compile_cmd(cmd, input)?;
1635            Statement::new(program, pager.clone(), mode, 0).run_ignore_rows()?;
1636        }
1637        Ok(())
1638    }
1639
1640    #[instrument(skip_all, level = Level::DEBUG)]
1641    pub fn query(self: &Arc<Connection>, sql: impl AsRef<str>) -> Result<Option<Statement>> {
1642        if self.is_closed() {
1643            return Err(LimboError::InternalError("Connection closed".to_string()));
1644        }
1645        let sql = sql.as_ref();
1646        tracing::trace!("Querying: {}", sql);
1647        let mut parser = Parser::new(sql.as_bytes());
1648        let cmd = parser.next_cmd()?;
1649        let byte_offset_end = parser.offset();
1650        let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
1651            .unwrap()
1652            .trim();
1653        match cmd {
1654            Some(cmd) => self.run_cmd(cmd, input),
1655            None => Ok(None),
1656        }
1657    }
1658
1659    #[instrument(skip_all, level = Level::DEBUG)]
1660    pub(crate) fn run_cmd(
1661        self: &Arc<Connection>,
1662        cmd: Cmd,
1663        input: &str,
1664    ) -> Result<Option<Statement>> {
1665        if self.is_closed() {
1666            return Err(LimboError::InternalError("Connection closed".to_string()));
1667        }
1668        let (program, pager, mode) = self.compile_cmd(cmd, input)?;
1669        let stmt = Statement::new(program, pager, mode, 0);
1670        Ok(Some(stmt))
1671    }
1672
1673    pub fn query_runner<'a>(self: &'a Arc<Connection>, sql: &'a [u8]) -> QueryRunner<'a> {
1674        QueryRunner::new(self, sql)
1675    }
1676
1677    /// Execute will run a query from start to finish taking ownership of I/O because it will run pending I/Os if it didn't finish.
1678    /// TODO: make this api async
1679    #[instrument(skip_all, level = Level::DEBUG)]
1680    #[turso_macros::trace_stack]
1681    pub fn execute(self: &Arc<Connection>, sql: impl AsRef<str>) -> Result<()> {
1682        if self.is_closed() {
1683            return Err(LimboError::InternalError("Connection closed".to_string()));
1684        }
1685        let sql = sql.as_ref();
1686        let mut parser = Parser::new(sql.as_bytes());
1687        while let Some(cmd) = parser.next_cmd()? {
1688            let byte_offset_end = parser.offset();
1689            let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
1690                .unwrap()
1691                .trim();
1692            let (program, pager, mode) = self.compile_cmd(cmd, input)?;
1693            {
1694                crate::stack::trace_stack!("run");
1695                Statement::new(program, pager.clone(), mode, 0).run_ignore_rows()?;
1696            }
1697        }
1698        Ok(())
1699    }
1700
1701    #[instrument(skip_all, level = Level::DEBUG)]
1702    pub fn consume_stmt(
1703        self: &Arc<Connection>,
1704        sql: impl AsRef<str>,
1705    ) -> Result<Option<(Statement, usize)>> {
1706        let mut parser = Parser::new(sql.as_ref().as_bytes());
1707        let Some(cmd) = parser.next_cmd()? else {
1708            return Ok(None);
1709        };
1710        let byte_offset_end = parser.offset();
1711        let input = str::from_utf8(&sql.as_ref().as_bytes()[..byte_offset_end])
1712            .unwrap()
1713            .trim();
1714        let (program, pager, mode) = self.compile_cmd(cmd, input)?;
1715        let stmt = Statement::new(program, pager, mode, 0);
1716        Ok(Some((stmt, parser.offset())))
1717    }
1718
1719    #[cfg(clt_turso_feature = "fs")]
1720    pub fn from_uri(uri: &str, db_opts: DatabaseOpts) -> Result<(Arc<dyn IO>, Arc<Connection>)> {
1721        use crate::util::MEMORY_PATH;
1722        let opts = OpenOptions::parse(uri)?;
1723        let flags = opts.get_flags()?;
1724        if opts.path == MEMORY_PATH || matches!(opts.mode, OpenMode::Memory) {
1725            let io = Arc::new(MemoryIO::new());
1726            let db = Database::open_file_with_flags(io.clone(), MEMORY_PATH, flags, db_opts, None)?;
1727            let conn = db.connect()?;
1728            return Ok((io, conn));
1729        }
1730        let encryption_opts = match (opts.cipher.clone(), opts.hexkey.clone()) {
1731            (Some(cipher), Some(hexkey)) => Some(EncryptionOpts { cipher, hexkey }),
1732            (Some(_), None) => {
1733                return Err(LimboError::InvalidArgument(
1734                    "hexkey is required when cipher is provided".to_string(),
1735                ));
1736            }
1737            (None, Some(_)) => {
1738                return Err(LimboError::InvalidArgument(
1739                    "cipher is required when hexkey is provided".to_string(),
1740                ));
1741            }
1742            (None, None) => None,
1743        };
1744        let (io, db) = Database::open_new(
1745            &opts.path,
1746            opts.vfs.as_ref(),
1747            flags,
1748            db_opts,
1749            encryption_opts,
1750        )?;
1751        if let Some(modeof) = opts.modeof {
1752            let perms = std::fs::metadata(modeof).map_err(|e| io_error(e, "metadata"))?;
1753            std::fs::set_permissions(&opts.path, perms.permissions())
1754                .map_err(|e| io_error(e, "set_permissions"))?;
1755        }
1756        let conn = db.connect()?;
1757        if let Some(cipher) = opts.cipher {
1758            let _ = conn.pragma_update("cipher", format!("'{cipher}'"));
1759        }
1760        if let Some(hexkey) = opts.hexkey {
1761            let _ = conn.pragma_update("hexkey", format!("'{hexkey}'"));
1762        }
1763        Ok((io, conn))
1764    }
1765
1766    #[cfg(clt_turso_feature = "fs")]
1767    fn from_uri_attached(
1768        uri: &str,
1769        mut db_opts: DatabaseOpts,
1770        main_db_flags: OpenFlags,
1771        io: Arc<dyn IO>,
1772    ) -> Result<(Arc<Database>, Option<EncryptionOpts>)> {
1773        let opts = OpenOptions::parse(uri)?;
1774        let mut flags = opts.get_flags()?;
1775        if main_db_flags.contains(OpenFlags::ReadOnly) {
1776            flags |= OpenFlags::ReadOnly;
1777        }
1778        let encryption_opts = match (opts.cipher.clone(), opts.hexkey.clone()) {
1779            (Some(cipher), Some(hexkey)) => Some(EncryptionOpts { cipher, hexkey }),
1780            (Some(_), None) => {
1781                return Err(LimboError::InvalidArgument(
1782                    "hexkey is required when cipher is provided".to_string(),
1783                ));
1784            }
1785            (None, Some(_)) => {
1786                return Err(LimboError::InvalidArgument(
1787                    "cipher is required when hexkey is provided".to_string(),
1788                ));
1789            }
1790            (None, None) => None,
1791        };
1792        if encryption_opts.is_some() {
1793            db_opts = db_opts.with_encryption(true);
1794        }
1795        let io = opts.vfs.map(Database::io_for_vfs).unwrap_or(Ok(io))?;
1796        let db = Database::open_file_with_flags(
1797            io.clone(),
1798            &opts.path,
1799            flags,
1800            db_opts,
1801            encryption_opts.clone(),
1802        )?;
1803        if let Some(modeof) = opts.modeof {
1804            let perms = std::fs::metadata(modeof).map_err(|e| io_error(e, "metadata"))?;
1805            std::fs::set_permissions(&opts.path, perms.permissions())
1806                .map_err(|e| io_error(e, "set_permissions"))?;
1807        }
1808        Ok((db, encryption_opts))
1809    }
1810
1811    pub fn set_foreign_keys_enabled(&self, enable: bool) {
1812        self.fk_pragma.store(enable, Ordering::Release);
1813        self.bump_prepare_context_generation();
1814    }
1815
1816    pub fn foreign_keys_enabled(&self) -> bool {
1817        self.fk_pragma.load(Ordering::Acquire)
1818    }
1819
1820    pub fn set_check_constraints_ignored(&self, ignore: bool) {
1821        self.check_constraints_pragma
1822            .store(ignore, Ordering::Release);
1823    }
1824
1825    pub fn check_constraints_ignored(&self) -> bool {
1826        self.check_constraints_pragma.load(Ordering::Acquire)
1827    }
1828
1829    pub(crate) fn clear_deferred_foreign_key_violations(&self) -> isize {
1830        self.fk_deferred_violations.swap(0, Ordering::Release)
1831    }
1832
1833    pub(crate) fn get_deferred_foreign_key_violations(&self) -> isize {
1834        self.fk_deferred_violations.load(Ordering::Acquire)
1835    }
1836
1837    pub(crate) fn increment_deferred_foreign_key_violations(&self, v: isize) {
1838        self.fk_deferred_violations.fetch_add(v, Ordering::AcqRel);
1839    }
1840
1841    /// Query the CREATE TYPE SQL definitions stored in __turso_internal_types.
1842    /// The connection's schema must already contain the table definitions so
1843    /// that `prepare` can resolve the table name. Returns an empty Vec if the
1844    /// types table does not exist.
1845    pub(crate) fn query_stored_type_definitions(self: &Arc<Connection>) -> Result<Vec<String>> {
1846        let has_types_table = {
1847            let s = self.schema.read();
1848            s.tables.contains_key(crate::schema::TURSO_TYPES_TABLE_NAME)
1849        };
1850        if !has_types_table {
1851            return Ok(Vec::new());
1852        }
1853        let mut type_stmt = self.prepare_internal(format!(
1854            "SELECT name, sql FROM {}",
1855            crate::schema::TURSO_TYPES_TABLE_NAME
1856        ))?;
1857        let mut type_rows = Vec::new();
1858        type_stmt.run_with_row_callback(|row| {
1859            type_rows.push(row.get::<&str>(1)?.to_string());
1860            Ok(())
1861        })?;
1862        Ok(type_rows)
1863    }
1864
1865    pub fn maybe_update_schema(&self) {
1866        if self.schema_reparse_in_progress() {
1867            return;
1868        }
1869        let current_schema = self.schema.read().clone();
1870        let schema = self.db.schema.lock();
1871        // MVCC checkpoint can publish physical btree roots into the shared
1872        // schema without changing SQLite's schema cookie. If this connection
1873        // still has the older schema snapshot, prepared statements must be
1874        // invalidated and recompiled with the published roots.
1875        if self.has_no_open_transaction_state()
1876            && (current_schema.schema_version != schema.schema_version
1877                || self
1878                    .has_mvcc_schema_snapshot_changed_with_same_version(&current_schema, &schema))
1879        {
1880            let mut adopted = schema.clone();
1881            // Resolve placeholder (negative) roots to the real pages a checkpoint has
1882            // materialized, so consumers that skip negative roots (integrity_check) see them.
1883            let mv_store_guard = self.db.get_mv_store();
1884            if let Some(mv_store) = mv_store_guard.as_ref() {
1885                if let Ok(schema) = Schema::try_make_mut(&mut adopted) {
1886                    mv_store.resolve_schema_negative_roots(schema);
1887                }
1888            }
1889            *self.schema.write() = adopted;
1890            self.bump_prepare_context_generation();
1891        }
1892    }
1893
1894    fn has_no_open_transaction_state(&self) -> bool {
1895        matches!(self.get_tx_state(), TransactionState::None)
1896            && self.get_mv_tx().is_none()
1897            && self.next_attached_mv_tx().is_none()
1898    }
1899
1900    fn has_mvcc_schema_snapshot_changed_with_same_version(
1901        &self,
1902        current_schema: &Arc<Schema>,
1903        schema: &Arc<Schema>,
1904    ) -> bool {
1905        self.mvcc_enabled()
1906            && current_schema.schema_version == schema.schema_version
1907            && !Arc::ptr_eq(current_schema, schema)
1908    }
1909
1910    pub(crate) fn mvcc_schema_requires_reprepare_before_tx(&self) -> bool {
1911        if !self.has_no_open_transaction_state() {
1912            return false;
1913        }
1914        let current_schema = self.schema.read().clone();
1915        let schema = self.db.schema.lock();
1916        self.has_mvcc_schema_snapshot_changed_with_same_version(&current_schema, &schema)
1917    }
1918
1919    /// Begin-tx schema gate for MVCC. Returns the `MvStore::schema_generation` this connection's
1920    /// prepared schema is valid as of, or `SchemaUpdated` if it is already stale (a passive
1921    /// checkpoint republished physical roots without a cookie change). The returned generation is
1922    /// re-checked inside `begin_tx`'s clock callback: a publish bumps `schema_generation` under the
1923    /// same clock, so if one lands between here and the begin clock the generations differ and the
1924    /// statement is forced to reprepare against the published roots.
1925    pub(crate) fn mvcc_begin_schema_generation(&self) -> Result<Option<u64>> {
1926        let mv_guard = self.db.get_mv_store();
1927        let Some(mv) = mv_guard.as_ref() else {
1928            return Ok(None);
1929        };
1930        // Mid-transaction (e.g. a multi-statement BEGIN): the snapshot and schema are fixed at the
1931        // first begin, so a later checkpoint republication must not gate or reprepare here. Mirror
1932        // the guard of `mvcc_schema_requires_reprepare_before_tx`.
1933        if !self.has_no_open_transaction_state() {
1934            return Ok(None);
1935        }
1936        // Read the generation before the snapshot comparison: any publish that mutates the shared
1937        // schema after this read is caught by the comparison below (it changes the Arc), and any
1938        // publish that lands during begin is caught by the clock re-check (it bumps the generation).
1939        let generation = mv.schema_generation();
1940        let current_schema = self.schema.read().clone();
1941        let schema = self.db.schema.lock();
1942        if self.has_mvcc_schema_snapshot_changed_with_same_version(&current_schema, &schema) {
1943            return Err(LimboError::SchemaUpdated);
1944        }
1945        Ok(Some(generation))
1946    }
1947
1948    pub(crate) fn refresh_schema_from_shared_for_reprepare(&self) {
1949        let current_schema = self.schema.read().clone();
1950        let schema = self.db.schema.lock().clone();
1951        if current_schema.schema_version < schema.schema_version
1952            || (self.has_no_open_transaction_state()
1953                && self
1954                    .has_mvcc_schema_snapshot_changed_with_same_version(&current_schema, &schema))
1955        {
1956            *self.schema.write() = schema;
1957            self.bump_prepare_context_generation();
1958        }
1959    }
1960
1961    /// Read schema version at current transaction
1962    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
1963    pub fn read_schema_version(&self) -> Result<u32> {
1964        let pager = self.pager.load();
1965        pager
1966            .io
1967            .block(|| pager.with_header(|header| header.schema_cookie))
1968            .map(|version| version.get())
1969    }
1970
1971    /// Update schema version to the new value within opened write transaction
1972    ///
1973    /// New version of the schema must be strictly greater than previous one - otherwise method will panic
1974    /// Write transaction must be opened in advance - otherwise method will panic
1975    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
1976    pub fn write_schema_version(self: &Arc<Connection>, version: u32) -> Result<()> {
1977        let TransactionState::Write { .. } = self.get_tx_state() else {
1978            return Err(LimboError::InternalError(
1979                "write_schema_version must be called from within Write transaction".to_string(),
1980            ));
1981        };
1982        let pager = self.pager.load();
1983        pager.io.block(|| {
1984            pager.with_header_mut(|header| {
1985                turso_assert!(
1986                    header.schema_cookie.get() < version,
1987                    "cookie can't go back in time"
1988                );
1989                self.with_schema_mut(|schema| schema.schema_version = version)
1990                    .map(|()| {
1991                        self.set_tx_state(TransactionState::Write {
1992                            schema_did_change: true,
1993                        });
1994                        header.schema_cookie = version.into();
1995                    })
1996            })
1997        })??;
1998        self.reparse_schema()?;
1999        Ok(())
2000    }
2001
2002    /// Try to read page with given ID with fixed WAL watermark position
2003    /// This method return false if page is not found (so, this is probably new page created after watermark position which wasn't checkpointed to the DB file yet)
2004    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2005    pub fn try_wal_watermark_read_page(
2006        &self,
2007        page_idx: u32,
2008        page: &mut [u8],
2009        frame_watermark: Option<u64>,
2010    ) -> Result<bool> {
2011        let Some((page_ref, c)) =
2012            self.try_wal_watermark_read_page_begin(page_idx, frame_watermark)?
2013        else {
2014            return Ok(false);
2015        };
2016        match self.get_pager().io.wait_for_completion(c) {
2017            Err(LimboError::CompletionError(err))
2018                if Self::wal_watermark_read_error_is_absent_page(&err) =>
2019            {
2020                return Ok(false);
2021            }
2022            Err(e) => return Err(e),
2023            _ => {}
2024        }
2025
2026        self.try_wal_watermark_read_page_end(page, page_ref)
2027    }
2028
2029    /// Classify a completion error raised while reading a page at a fixed WAL
2030    /// watermark. On Windows under `experimental_win_iocp`, an absent /
2031    /// zero-length page read surfaces as `UnexpectedEof` (see
2032    /// `core/io/win_iocp.rs`); every watermark-read site must treat that as
2033    /// "page absent" (size 0) rather than a hard error. Centralized here so the
2034    /// platform handling cannot drift across the (now four) call sites.
2035    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2036    pub fn wal_watermark_read_error_is_absent_page(err: &crate::error::CompletionError) -> bool {
2037        #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
2038        {
2039            matches!(
2040                err,
2041                crate::error::CompletionError::IOError(std::io::ErrorKind::UnexpectedEof, _)
2042            )
2043        }
2044        #[cfg(not(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp")))]
2045        {
2046            let _ = err;
2047            false
2048        }
2049    }
2050
2051    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2052    pub fn try_wal_watermark_read_page_begin(
2053        &self,
2054        page_idx: u32,
2055        frame_watermark: Option<u64>,
2056    ) -> Result<Option<(Arc<Page>, Completion)>> {
2057        let pager = self.pager.load();
2058        let (page_ref, c) = match pager.read_page_no_cache(page_idx as i64, frame_watermark, true) {
2059            Ok(result) => result,
2060            // on windows, zero read will trigger UnexpectedEof
2061            #[cfg(target_os = "windows")]
2062            Err(LimboError::CompletionError(crate::error::CompletionError::IOError(
2063                std::io::ErrorKind::UnexpectedEof,
2064                _,
2065            ))) => return Ok(None),
2066            Err(err) => return Err(err),
2067        };
2068
2069        Ok(Some((page_ref, c)))
2070    }
2071
2072    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2073    pub fn try_wal_watermark_read_page_end(
2074        &self,
2075        page: &mut [u8],
2076        page_ref: Arc<Page>,
2077    ) -> Result<bool> {
2078        let content = page_ref.get_contents();
2079        // empty read - attempt to read absent page
2080        if content.buffer.as_ref().is_none_or(|b| b.is_empty()) {
2081            return Ok(false);
2082        }
2083        page.copy_from_slice(content.as_ptr());
2084        Ok(true)
2085    }
2086
2087    /// Return unique set of page numbers changes after WAL watermark position in the current WAL session
2088    /// (so, if concurrent connection wrote something to the WAL - this method will not see this change)
2089    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2090    pub fn wal_changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>> {
2091        self.pager.load().wal_changed_pages_after(frame_watermark)
2092    }
2093
2094    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2095    pub fn wal_state(&self) -> Result<WalState> {
2096        self.pager.load().wal_state()
2097    }
2098
2099    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2100    pub fn wal_get_frame(&self, frame_no: u64, frame: &mut [u8]) -> Result<WalFrameInfo> {
2101        use crate::storage::sqlite3_ondisk::parse_wal_frame_header;
2102
2103        let c = self.pager.load().wal_get_frame(frame_no, frame)?;
2104        self.db.io.wait_for_completion(c)?;
2105        let (header, _) = parse_wal_frame_header(frame);
2106        Ok(WalFrameInfo {
2107            page_no: header.page_number,
2108            db_size: header.db_size,
2109        })
2110    }
2111
2112    /// Insert `frame` (header included) at the position `frame_no` in the WAL
2113    /// If WAL already has frame at that position - turso-db will compare content of the page and either report conflict or return OK
2114    /// If attempt to write frame at the position `frame_no` will create gap in the WAL - method will return error
2115    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2116    pub fn wal_insert_frame(&self, frame_no: u64, frame: &[u8]) -> Result<WalFrameInfo> {
2117        self.pager.load().wal_insert_frame(frame_no, frame)
2118    }
2119
2120    /// Start WAL session by initiating read+write transaction for this connection
2121    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2122    pub fn wal_insert_begin(&self) -> Result<()> {
2123        let pager = self.pager.load();
2124        pager.begin_read_tx()?;
2125        // Sync-engine drives WAL maintenance explicitly: any auto-restart of
2126        // the WAL header here would invalidate the watermarks the caller has
2127        // already published (see `wal_changed_pages_after`), so opt out of
2128        // every auto action for this write transaction.
2129        pager
2130            .io
2131            .block(|| pager.begin_write_tx(WalAutoActions::empty()))
2132            .inspect_err(|_| {
2133                pager.end_read_tx();
2134            })?;
2135
2136        // start write transaction and disable auto-commit mode as SQL can be executed within WAL session (at caller own risk)
2137        self.set_tx_state(TransactionState::Write {
2138            schema_did_change: false,
2139        });
2140        self.auto_commit.store(false, Ordering::SeqCst);
2141
2142        Ok(())
2143    }
2144
2145    /// Finish WAL session by ending read+write transaction taken in the [Self::wal_insert_begin] method
2146    /// All frames written after last commit frame (db_size > 0) within the session will be rolled back
2147    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2148    pub fn wal_insert_end(self: &Arc<Connection>, force_commit: bool) -> Result<()> {
2149        use crate::{return_if_io, types::IOResult};
2150
2151        {
2152            let pager = self.pager.load();
2153
2154            let Some(wal) = pager.wal.as_ref() else {
2155                return Err(LimboError::InternalError(
2156                    "wal_insert_end called without a wal".to_string(),
2157                ));
2158            };
2159
2160            let commit_err = if force_commit {
2161                pager
2162                    .io
2163                    .block(|| {
2164                        return_if_io!(pager.commit_wal(
2165                            WalAutoActions::empty(),
2166                            self.get_sync_mode(),
2167                            self.get_data_sync_retry(),
2168                        ));
2169                        pager.commit_wal_end();
2170                        Ok(IOResult::Done(()))
2171                    })
2172                    .err()
2173            } else {
2174                None
2175            };
2176
2177            self.auto_commit.store(true, Ordering::SeqCst);
2178            self.set_tx_state(TransactionState::None);
2179            wal.end_write_tx();
2180            wal.end_read_tx();
2181
2182            if !force_commit {
2183                // remove all non-commited changes in case if WAL session left some suffix without commit frame
2184                if let Some(mv_store) = self.mv_store().as_ref() {
2185                    if let Some(tx_id) = self.get_mv_tx_id() {
2186                        mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID);
2187                    }
2188                }
2189                pager.rollback(false, self, true);
2190            }
2191            if let Some(err) = commit_err {
2192                return Err(err);
2193            }
2194        }
2195
2196        // let's re-parse schema from scratch if schema cookie changed compared to the our in-memory view of schema
2197        self.maybe_reparse_schema()?;
2198        Ok(())
2199    }
2200
2201    /// Flush dirty pages to disk.
2202    pub fn cacheflush(&self) -> Result<Vec<Completion>> {
2203        if self.is_closed() {
2204            return Err(LimboError::InternalError("Connection closed".to_string()));
2205        }
2206        let pager = self.pager.load();
2207        pager.io.block(|| pager.cacheflush())
2208    }
2209
2210    pub fn checkpoint(self: &Arc<Self>, mode: CheckpointMode) -> Result<CheckpointResult> {
2211        use crate::mvcc::database::CheckpointStateMachine;
2212        use crate::state_machine::{StateTransition, TransitionResult};
2213        if self.is_closed() {
2214            return Err(LimboError::InternalError("Connection closed".to_string()));
2215        }
2216        if let Some(mv_store) = self.mv_store().as_ref() {
2217            let pager = self.pager.load().clone();
2218            let io = pager.io.clone();
2219            let mut ckpt_sm = CheckpointStateMachine::new(
2220                pager,
2221                mv_store.clone(),
2222                self.clone(),
2223                true,
2224                self.get_sync_mode(),
2225                MAIN_DB_ID,
2226                // Explicit Connection::checkpoint fully resets the WAL.
2227                crate::storage::wal::CheckpointMode::Truncate {
2228                    upper_bound_inclusive: None,
2229                },
2230            );
2231            loop {
2232                match ckpt_sm.step(&()) {
2233                    Ok(TransitionResult::Continue) => {}
2234                    Ok(TransitionResult::Done(result)) => return Ok(result),
2235                    Ok(TransitionResult::Io(iocompletions)) => {
2236                        if let Err(err) = iocompletions.wait(io.as_ref()) {
2237                            ckpt_sm.cleanup_after_external_io_error(err.clone())?;
2238                            return Err(err);
2239                        }
2240                    }
2241                    Err(err) => return Err(err),
2242                }
2243            }
2244        } else {
2245            self.pager
2246                .load()
2247                .blocking_checkpoint(mode, self.get_sync_mode())
2248        }
2249    }
2250
2251    /// Close a connection and checkpoint.
2252    pub fn close(&self) -> Result<()> {
2253        if self.is_closed() {
2254            return Ok(());
2255        }
2256        self.closed.store(true, Ordering::SeqCst);
2257        let pager = self.pager.load();
2258
2259        match self.get_tx_state() {
2260            TransactionState::None => {
2261                // No active transaction
2262            }
2263            _ => {
2264                if self.mvcc_enabled() {
2265                    if let Some(mv_store) = self.mv_store().as_ref() {
2266                        if let Some(tx_id) = self.get_mv_tx_id() {
2267                            mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID);
2268                        }
2269                    }
2270                    pager.end_read_tx();
2271                } else {
2272                    pager.rollback_tx(self);
2273                }
2274                // Roll back all attached DB transactions regardless of main
2275                // DB mode — a :memory: attached DB may use WAL even when the
2276                // main DB uses MVCC.
2277                self.rollback_attached_mvcc_txs(false);
2278                self.rollback_attached_wal_txns();
2279                self.set_tx_state(TransactionState::None);
2280            }
2281        }
2282        self.clear_mvcc_log_meta();
2283
2284        let is_memory_db = is_memory_like(&self.db.path);
2285        let should_checkpoint_on_close = pager
2286            .wal
2287            .as_ref()
2288            .is_none_or(|wal| wal.should_checkpoint_on_close());
2289        if self.db.n_connections.fetch_sub(1, Ordering::SeqCst).eq(&1)
2290            && !self.db.is_readonly()
2291            && !is_memory_db
2292            && should_checkpoint_on_close
2293        {
2294            self.pager
2295                .load()
2296                .checkpoint_shutdown(self.wal_auto_actions(), self.get_sync_mode())?;
2297        };
2298        Ok(())
2299    }
2300
2301    /// Disable every automatic WAL maintenance action for this connection
2302    /// (auto-checkpoint AND WAL header restart). Sync-engine consumers call
2303    /// this so they own all WAL bookkeeping themselves.
2304    pub fn wal_auto_actions_disable(&self) {
2305        self.wal_auto_actions
2306            .store(WalAutoActions::empty().bits(), Ordering::SeqCst);
2307    }
2308
2309    /// Returns the set of automatic WAL maintenance actions this connection
2310    /// permits. MVCC connections always return an empty set because the
2311    /// MVCC checkpoint state machine drives WAL maintenance explicitly.
2312    pub fn wal_auto_actions(&self) -> WalAutoActions {
2313        if self.db.get_mv_store().is_some() {
2314            return WalAutoActions::empty();
2315        }
2316        WalAutoActions::from_bits_truncate(self.wal_auto_actions.load(Ordering::SeqCst))
2317    }
2318
2319    /// Publish the connection's current schema snapshot to the shared database
2320    /// cache after a successful commit so other live connections can refresh.
2321    pub fn publish_schema_if_newer(&self) {
2322        let schema = self.schema.read().clone();
2323        self.db.update_schema_if_newer(schema);
2324    }
2325
2326    /// Publish the connection's current schema snapshot after pages were
2327    /// replaced outside normal SQL commit ordering.
2328    ///
2329    /// External restore paths can move the schema cookie backwards. In that
2330    /// case the shared schema cache must be replaced rather than updated
2331    /// monotonically, otherwise new connections can re-adopt stale metadata.
2332    #[cfg(clt_turso_feature = "conn_raw_api")]
2333    pub fn publish_schema_after_external_restore(&self) -> Result<()> {
2334        if self.get_tx_state() != TransactionState::None {
2335            return Err(LimboError::Busy);
2336        }
2337        if self.get_mv_tx().is_some() || self.next_attached_mv_tx().is_some() {
2338            return Err(LimboError::Busy);
2339        }
2340
2341        let schema = self.schema.read().clone();
2342        self.db.with_schema_mut(|current| {
2343            *current = schema.as_ref().try_clone()?;
2344            Ok(())
2345        })?;
2346        Ok(())
2347    }
2348
2349    /// Roll back the main-database MVCC transaction while keeping the
2350    /// surrounding raw WAL-insert session open.
2351    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2352    pub fn reset_main_mvcc_tx_for_wal_session(&self) {
2353        let mv_store = self.mv_store();
2354        let Some(mv_store) = mv_store.as_ref() else {
2355            return;
2356        };
2357        let Some(tx_id) = self.get_mv_tx_id() else {
2358            return;
2359        };
2360        let pager = self.pager.load();
2361        mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID);
2362    }
2363
2364    /// Discard the main-db MVCC transaction left by a sync raw-WAL session
2365    /// before reparsing state after external file replacement.
2366    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2367    pub fn discard_main_mvcc_tx_after_external_restore(&self) {
2368        let pager = self.pager.load();
2369        self.clear_internal_main_mvcc_tx(&pager);
2370    }
2371
2372    /// Returns whether the main database currently has a live MVCC transaction.
2373    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2374    pub fn has_main_mvcc_tx_for_wal_session(&self) -> bool {
2375        self.get_mv_tx_id().is_some()
2376    }
2377
2378    /// Commit the main-database MVCC transaction while keeping the surrounding
2379    /// raw WAL-insert session open.
2380    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
2381    pub fn commit_main_mvcc_tx_for_wal_session(self: &Arc<Self>) -> Result<()> {
2382        let mv_store_handle = self.mv_store();
2383        let Some(mv_store) = mv_store_handle.as_ref() else {
2384            return Ok(());
2385        };
2386        let Some(tx_id) = self.get_mv_tx_id() else {
2387            return Ok(());
2388        };
2389
2390        let mut state_machine = mv_store.commit_tx(tx_id, self, MAIN_DB_ID)?;
2391        while let IOResult::IO(io) = state_machine.step(mv_store)? {
2392            io.wait(self.db.io.as_ref())?;
2393        }
2394        assert!(state_machine.is_finalized());
2395        self.set_mv_tx(None);
2396        self.publish_schema_if_newer();
2397        Ok(())
2398    }
2399
2400    #[cfg(clt_turso_feature = "conn_raw_api")]
2401    pub fn reload_wal_after_external_restore(&self) -> Result<()> {
2402        self.db.reload_wal_after_external_restore()
2403    }
2404
2405    /// Enable or disable writing portable logical-change metadata into MVCC
2406    /// logical-log frames.
2407    pub fn set_portable_logical_changes_enabled(&self, enabled: bool) {
2408        #[cfg(clt_turso_feature = "conn_raw_api")]
2409        {
2410            self.portable_logical_changes_enabled
2411                .store(enabled, Ordering::Release);
2412        }
2413        let _ = enabled;
2414    }
2415
2416    pub fn portable_logical_changes_enabled(&self) -> bool {
2417        #[cfg(clt_turso_feature = "conn_raw_api")]
2418        {
2419            self.portable_logical_changes_enabled
2420                .load(Ordering::Acquire)
2421        }
2422        #[cfg(not(clt_turso_feature = "conn_raw_api"))]
2423        {
2424            false
2425        }
2426    }
2427
2428    pub fn set_mvcc_log_meta(&self, key: String, value: Option<String>) {
2429        #[cfg(clt_turso_feature = "conn_raw_api")]
2430        {
2431            let mut metadata = self.mvcc_log_metadata.write();
2432            match value {
2433                Some(value) => {
2434                    metadata.insert(key, value);
2435                }
2436                None => {
2437                    metadata.remove(&key);
2438                }
2439            }
2440        }
2441        #[cfg(not(clt_turso_feature = "conn_raw_api"))]
2442        {
2443            let _ = (key, value);
2444        }
2445    }
2446
2447    #[cfg(clt_turso_feature = "conn_raw_api")]
2448    pub(crate) fn mvcc_log_meta_snapshot(&self) -> HashMap<String, String> {
2449        self.mvcc_log_metadata.read().clone()
2450    }
2451
2452    pub(crate) fn clear_mvcc_log_meta(&self) {
2453        #[cfg(clt_turso_feature = "conn_raw_api")]
2454        {
2455            self.mvcc_log_metadata.write().clear();
2456        }
2457    }
2458
2459    pub fn mvcc_log_meta(&self, key: &str) -> Option<String> {
2460        #[cfg(clt_turso_feature = "conn_raw_api")]
2461        {
2462            return self.mvcc_log_metadata.read().get(key).cloned();
2463        }
2464        #[cfg(not(clt_turso_feature = "conn_raw_api"))]
2465        {
2466            let _ = key;
2467            None
2468        }
2469    }
2470
2471    #[cfg(clt_turso_feature = "simulator")]
2472    pub fn checkpoint_for_testing(&self, mode: CheckpointMode) -> Result<CheckpointResult> {
2473        let pager = self.pager.load();
2474        pager
2475            .io
2476            .block(|| pager.checkpoint(mode, SyncMode::Full, true))
2477    }
2478
2479    #[cfg(all(
2480        clt_turso_feature = "simulator",
2481        target_pointer_width = "64",
2482        host_shared_wal
2483    ))]
2484    pub fn install_unpublished_backfill_proof_for_testing(
2485        &self,
2486        upper_bound_inclusive: u64,
2487    ) -> Result<()> {
2488        let pager = self.pager.load();
2489        let proof_nbackfills =
2490            pager.run_checkpoint_until_post_sync_gap_for_testing(CheckpointMode::Passive {
2491                upper_bound_inclusive: Some(upper_bound_inclusive),
2492            })?;
2493        let authority = self.db.shared_wal_coordination()?.ok_or_else(|| {
2494            LimboError::InternalError("shared WAL authority is unavailable".into())
2495        })?;
2496        let snapshot_before_publish = authority.snapshot();
2497        if snapshot_before_publish.nbackfills != 0 {
2498            return Err(LimboError::InternalError(
2499                "unpublished-proof setup requires nbackfills to remain unpublished".into(),
2500            ));
2501        }
2502
2503        let (db_size_pages, db_header_crc32c) = db_identity_for_testing(Path::new(&self.db.path))?;
2504        authority.install_backfill_proof(
2505            crate::storage::shared_wal_coordination::SharedWalCoordinationHeader {
2506                nbackfills: proof_nbackfills,
2507                ..snapshot_before_publish
2508            },
2509            db_size_pages,
2510            db_header_crc32c,
2511        );
2512        Ok(())
2513    }
2514
2515    pub fn last_insert_rowid(&self) -> i64 {
2516        self.last_insert_rowid.load(Ordering::SeqCst)
2517    }
2518
2519    pub(crate) fn update_last_rowid(&self, rowid: i64) {
2520        self.last_insert_rowid.store(rowid, Ordering::SeqCst);
2521    }
2522
2523    pub(crate) fn add_total_changes(&self, num_changes: i64) {
2524        self.total_changes.fetch_add(num_changes, Ordering::SeqCst);
2525    }
2526
2527    pub fn set_changes(&self, num_changes: i64) {
2528        self.changes.store(num_changes, Ordering::SeqCst);
2529    }
2530
2531    pub fn changes(&self) -> i64 {
2532        self.changes.load(Ordering::SeqCst)
2533    }
2534
2535    pub fn total_changes(&self) -> i64 {
2536        self.total_changes.load(Ordering::SeqCst)
2537    }
2538
2539    pub fn get_cache_size(&self) -> i32 {
2540        self.cache_size.load(Ordering::SeqCst)
2541    }
2542    pub fn set_cache_size(&self, size: i32) {
2543        self.cache_size.store(size, Ordering::SeqCst);
2544        self.bump_prepare_context_generation();
2545    }
2546
2547    pub fn get_capture_data_changes_info(
2548        &self,
2549    ) -> crate::sync::RwLockReadGuard<'_, Option<CaptureDataChangesInfo>> {
2550        self.capture_data_changes.read()
2551    }
2552    pub fn set_capture_data_changes_info(&self, opts: Option<CaptureDataChangesInfo>) {
2553        *self.capture_data_changes.write() = opts;
2554        self.bump_prepare_context_generation();
2555    }
2556    pub fn get_cdc_transaction_id(&self) -> i64 {
2557        self.cdc_transaction_id.load(Ordering::SeqCst)
2558    }
2559    pub fn set_cdc_transaction_id(&self, id: i64) {
2560        self.cdc_transaction_id.store(id, Ordering::SeqCst);
2561    }
2562    pub fn get_page_size(&self) -> PageSize {
2563        let value = self.page_size.load(Ordering::SeqCst);
2564        PageSize::new_from_header_u16(value).unwrap_or_default()
2565    }
2566
2567    pub fn is_closed(&self) -> bool {
2568        self.closed.load(Ordering::SeqCst)
2569    }
2570
2571    pub fn is_query_only(&self) -> bool {
2572        self.query_only.load(Ordering::SeqCst)
2573    }
2574
2575    pub fn get_database_canonical_path(&self) -> String {
2576        self.db.get_database_canonical_path()
2577    }
2578
2579    /// Check if a specific attached database is read only or not, by its index
2580    pub fn is_readonly(&self, index: usize) -> bool {
2581        match index {
2582            crate::MAIN_DB_ID => self.db.is_readonly(),
2583            crate::TEMP_DB_ID => self
2584                .temp
2585                .database
2586                .read()
2587                .as_ref()
2588                .is_some_and(|temp_db| temp_db.db.is_readonly()),
2589            _ => {
2590                let db = self.attached_databases.read().get_database_by_index(index);
2591                db.expect("Should never have called this without being sure the database exists")
2592                    .is_readonly()
2593            }
2594        }
2595    }
2596
2597    /// Reset the page size for the current connection.
2598    ///
2599    /// Specifying a new page size does not change the page size immediately.
2600    /// Instead, the new page size is remembered and is used to set the page size when the database
2601    /// is first created, if it does not already exist when the page_size pragma is issued,
2602    /// or at the next VACUUM command that is run on the same database connection while not in WAL mode.
2603    pub fn reset_page_size(&self, size: u32) -> Result<()> {
2604        if self.db.initialized() {
2605            return Ok(());
2606        }
2607        let Some(size) = PageSize::new(size) else {
2608            return Ok(());
2609        };
2610
2611        self.page_size.store(size.get_raw(), Ordering::SeqCst);
2612        self.pager.load().set_initial_page_size(size)?;
2613        // MvStore caches a copy of the database header in `global_header`, captured from the
2614        // pager during bootstrap (before any PRAGMA page_size can run). Propagate the new
2615        // page size so subsequent transactions and any header lookups see the same value the
2616        // pager will write to disk; otherwise paths like op_open_ephemeral allocate buffers
2617        // sized to the connection's page_size but compute usable_space from the stale 4 KiB
2618        // global header, tripping the btree_init_page assertion.
2619        if let Some(mv_store) = self.db.get_mv_store().as_ref() {
2620            mv_store.set_global_page_size(size);
2621        }
2622        self.bump_prepare_context_generation();
2623
2624        Ok(())
2625    }
2626
2627    #[cfg(clt_turso_feature = "fs")]
2628    pub fn open_new(&self, path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
2629        Database::open_with_vfs(&self.db, path, vfs)
2630    }
2631
2632    pub fn list_vfs(&self) -> Vec<String> {
2633        #[allow(unused_mut)]
2634        let mut all_vfs = vec![String::from("memory")];
2635        #[cfg(clt_turso_feature = "fs")]
2636        {
2637            #[cfg(target_family = "unix")]
2638            {
2639                all_vfs.push("syscall".to_string());
2640            }
2641            #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring"))]
2642            {
2643                all_vfs.push("io_uring".to_string());
2644            }
2645            #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
2646            {
2647                all_vfs.push("experimental_win_iocp".to_string());
2648            }
2649            all_vfs.extend(crate::ext::list_vfs_modules());
2650        }
2651        all_vfs
2652    }
2653
2654    pub fn get_auto_commit(&self) -> bool {
2655        self.auto_commit.load(Ordering::SeqCst)
2656    }
2657
2658    /// Mark the active explicit transaction poisoned so COMMIT rolls it back.
2659    ///
2660    /// This is used when a write statement under BEGIN is abandoned before it
2661    /// reaches Halt/Done and that statement did not open a statement savepoint.
2662    pub(crate) fn mark_tx_poisoned(&self) {
2663        self.poisoned_tx.store(true, Ordering::SeqCst);
2664    }
2665
2666    /// Return whether the active explicit transaction must roll back at COMMIT.
2667    pub(crate) fn tx_is_poisoned(&self) -> bool {
2668        self.poisoned_tx.load(Ordering::SeqCst)
2669    }
2670
2671    /// Clear the poison marker after BEGIN, COMMIT, or ROLLBACK.
2672    pub(crate) fn clear_tx_poison(&self) {
2673        self.poisoned_tx.store(false, Ordering::SeqCst);
2674    }
2675
2676    pub fn set_load_extension_enabled(&self, enabled: bool) {
2677        self.enable_load_extension.store(enabled, Ordering::Release);
2678    }
2679
2680    pub(crate) fn can_load_extensions(&self) -> bool {
2681        self.enable_load_extension.load(Ordering::Acquire)
2682    }
2683
2684    pub fn reparse_schema_after_extension_load(self: &Arc<Connection>) -> Result<()> {
2685        if self.is_closed() {
2686            return Err(LimboError::InternalError("Connection closed".to_string()));
2687        }
2688        // Collect row data from the Statement first, then drop the Statement
2689        // before taking the schema write lock. This prevents a deadlock in MVCC
2690        // mode where Statement::drop -> abort -> rollback_tx -> schema.read()
2691        // would deadlock against the schema write lock.
2692        let mut rows_data: Vec<(String, String, String, i64, Option<String>)> = Vec::new();
2693        {
2694            let mut rows = self
2695                .query("SELECT * FROM sqlite_schema")?
2696                .expect("query must be parsed to statement");
2697            rows.run_with_row_callback(|row| {
2698                let ty = row.get::<&str>(0)?.to_string();
2699                let name = row.get::<&str>(1)?.to_string();
2700                let table_name = row.get::<&str>(2)?.to_string();
2701                let root_page = row.get::<i64>(3)?;
2702                let sql = row.get::<&str>(4).ok().map(|s| s.to_string());
2703                rows_data.push((ty, name, table_name, root_page, sql));
2704                Ok(())
2705            })?;
2706        } // Statement dropped here, before schema write lock
2707
2708        let syms = self.syms.read();
2709        self.with_schema_mut(|schema| -> Result<()> {
2710            // Incremental re-parse after extension loading. The schema already has
2711            // tables/indices/views from initial parse. We only need to pick up
2712            // entries that previously failed (e.g. virtual tables whose module
2713            // wasn't loaded yet). "Already exists" errors are expected and skipped.
2714            let mut from_sql_indexes = crate::alloc::vec![];
2715            let mut automatic_indices = HashMap::default();
2716            let mut dbsp_state_roots = HashMap::default();
2717            let mut dbsp_state_index_roots = HashMap::default();
2718            let mut materialized_view_info = HashMap::default();
2719
2720            let attached_resolver = |name: &str| -> Option<usize> {
2721                self.attached_databases
2722                    .read()
2723                    .get_database_by_name(&crate::util::normalize_ident(name))
2724                    .map(|(idx, _)| idx)
2725            };
2726            for (ty, name, table_name, root_page, sql) in &rows_data {
2727                match schema.handle_schema_row(
2728                    ty,
2729                    name,
2730                    table_name,
2731                    *root_page,
2732                    sql.as_deref(),
2733                    &syms,
2734                    &mut from_sql_indexes,
2735                    &mut automatic_indices,
2736                    &mut dbsp_state_roots,
2737                    &mut dbsp_state_index_roots,
2738                    &mut materialized_view_info,
2739                    &attached_resolver,
2740                ) {
2741                    Ok(()) => {}
2742                    Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {}
2743                    Err(LimboError::ExtensionError(msg)) => {
2744                        eprintln!("Warning: {msg}");
2745                    }
2746                    Err(e) => return Err(e),
2747                }
2748            }
2749
2750            match schema.populate_indices(&syms, from_sql_indexes, automatic_indices, false) {
2751                Ok(()) => {}
2752                Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {}
2753                Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"),
2754                Err(e) => return Err(e),
2755            }
2756            match schema.populate_materialized_views(
2757                materialized_view_info,
2758                dbsp_state_roots,
2759                dbsp_state_index_roots,
2760            ) {
2761                Ok(()) => {}
2762                Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"),
2763                Err(e) => return Err(e),
2764            }
2765            Ok(())
2766        })?
2767    }
2768
2769    // Clearly there is something to improve here, Vec<Vec<Value>> isn't a couple of tea
2770    /// Query the current rows/values of `pragma_name`.
2771    pub fn pragma_query(self: &Arc<Connection>, pragma_name: &str) -> Result<Vec<Vec<Value>>> {
2772        if self.is_closed() {
2773            return Err(LimboError::InternalError("Connection closed".to_string()));
2774        }
2775        let pragma = format!("PRAGMA {pragma_name}");
2776        let mut stmt = self.prepare(pragma)?;
2777        stmt.run_collect_rows()
2778    }
2779
2780    /// Set a new value to `pragma_name`.
2781    ///
2782    /// Some pragmas will return the updated value which cannot be retrieved
2783    /// with this method.
2784    pub fn pragma_update<V: Display>(
2785        self: &Arc<Connection>,
2786        pragma_name: &str,
2787        pragma_value: V,
2788    ) -> Result<Vec<Vec<Value>>> {
2789        if self.is_closed() {
2790            return Err(LimboError::InternalError("Connection closed".to_string()));
2791        }
2792        let pragma = format!("PRAGMA {pragma_name} = {pragma_value}");
2793        let mut stmt = self.prepare(pragma)?;
2794        stmt.run_collect_rows()
2795    }
2796
2797    pub fn experimental_views_enabled(&self) -> bool {
2798        self.db.experimental_views_enabled()
2799    }
2800
2801    pub fn experimental_index_method_enabled(&self) -> bool {
2802        self.db.experimental_index_method_enabled()
2803    }
2804
2805    pub fn experimental_custom_types_enabled(&self) -> bool {
2806        self.db.experimental_custom_types_enabled()
2807    }
2808
2809    pub fn experimental_attach_enabled(&self) -> bool {
2810        self.db.experimental_attach_enabled()
2811    }
2812
2813    pub fn experimental_vacuum_enabled(&self) -> bool {
2814        self.db.experimental_vacuum_enabled()
2815    }
2816
2817    pub fn experimental_mvcc_passive_checkpoint_enabled(&self) -> bool {
2818        self.db.experimental_mvcc_passive_checkpoint_enabled()
2819    }
2820
2821    pub fn experimental_multiprocess_wal_enabled(&self) -> bool {
2822        self.db.experimental_multiprocess_wal_enabled()
2823    }
2824
2825    pub fn experimental_generated_columns_enabled(&self) -> bool {
2826        self.db.experimental_generated_columns_enabled()
2827    }
2828
2829    pub fn experimental_without_rowid_enabled(&self) -> bool {
2830        self.db.experimental_without_rowid_enabled()
2831    }
2832
2833    pub fn mvcc_enabled(&self) -> bool {
2834        self.db.mvcc_enabled()
2835    }
2836
2837    pub fn mv_store(&self) -> impl Deref<Target = Option<Arc<MvStore>>> {
2838        struct TransparentWrapper<T>(T);
2839
2840        impl<T> Deref for TransparentWrapper<T> {
2841            type Target = T;
2842
2843            fn deref(&self) -> &Self::Target {
2844                &self.0
2845            }
2846        }
2847
2848        // Never use MV store for bootstrapping - we read state directly from sqlite_schema in the DB file.
2849        if !self.is_mvcc_bootstrap_connection() {
2850            either::Left(self.db.get_mv_store())
2851        } else {
2852            either::Right(TransparentWrapper(None))
2853        }
2854    }
2855
2856    #[cfg(any(clt_turso_tests, injected_yields))]
2857    pub fn set_yield_injector(&self, injector: Option<Arc<dyn YieldInjector>>) {
2858        let mut slot = self.yield_injector.write();
2859        match injector {
2860            Some(injector) => {
2861                turso_assert!(
2862                    slot.is_none(),
2863                    "yield injector should be empty before installing a new one"
2864                );
2865                *slot = Some(injector);
2866            }
2867            None => {
2868                turso_assert!(
2869                    slot.is_some(),
2870                    "yield injector should be installed before it is cleared"
2871                );
2872                *slot = None;
2873            }
2874        }
2875    }
2876
2877    #[cfg(any(clt_turso_tests, injected_yields))]
2878    pub(crate) fn yield_injector(&self) -> Option<Arc<dyn YieldInjector>> {
2879        self.yield_injector.read().clone()
2880    }
2881
2882    #[cfg(any(clt_turso_tests, injected_yields))]
2883    pub fn set_failure_injector(&self, injector: Option<Arc<dyn FailureInjector>>) {
2884        let mut slot = self.failure_injector.write();
2885        match injector {
2886            Some(injector) => {
2887                turso_assert!(
2888                    slot.is_none(),
2889                    "failure injector should be empty before installing a new one"
2890                );
2891                *slot = Some(injector);
2892            }
2893            None => {
2894                turso_assert!(
2895                    slot.is_some(),
2896                    "failure injector should be installed before it is cleared"
2897                );
2898                *slot = None;
2899            }
2900        }
2901    }
2902
2903    #[cfg(any(clt_turso_tests, injected_yields))]
2904    pub(crate) fn failure_injector(&self) -> Option<Arc<dyn FailureInjector>> {
2905        self.failure_injector.read().clone()
2906    }
2907
2908    #[cfg(any(clt_turso_tests, injected_yields))]
2909    #[inline(always)]
2910    pub(crate) fn next_yield_instance_id(&self) -> u64 {
2911        self.yield_instance_id_counter
2912            .fetch_add(1, Ordering::Relaxed)
2913    }
2914
2915    /// Query the current value(s) of `pragma_name` associated to
2916    /// `pragma_value`.
2917    ///
2918    /// This method can be used with query-only pragmas which need an argument
2919    /// (e.g. `table_info('one_tbl')`) or pragmas which returns value(s)
2920    /// (e.g. `integrity_check`).
2921    pub fn pragma<V: Display>(
2922        self: &Arc<Connection>,
2923        pragma_name: &str,
2924        pragma_value: V,
2925    ) -> Result<Vec<Vec<Value>>> {
2926        if self.is_closed() {
2927            return Err(LimboError::InternalError("Connection closed".to_string()));
2928        }
2929        let pragma = format!("PRAGMA {pragma_name}({pragma_value})");
2930        let mut stmt = self.prepare(pragma)?;
2931        let mut results = Vec::new();
2932        loop {
2933            match stmt.step()? {
2934                vdbe::StepResult::Row => {
2935                    let row: Vec<Value> = stmt.row().unwrap().get_values().cloned().collect();
2936                    results.push(row);
2937                }
2938                vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => {
2939                    return Err(LimboError::Busy);
2940                }
2941                _ => break,
2942            }
2943        }
2944
2945        Ok(results)
2946    }
2947
2948    #[inline]
2949    pub fn with_schema_mut<T>(&self, f: impl FnOnce(&mut Schema) -> T) -> Result<T> {
2950        let mut schema_ref = self.schema.write();
2951        let schema = Schema::try_make_mut(&mut schema_ref)?;
2952        Ok(f(schema))
2953    }
2954
2955    /// Mutate the schema for a specific database (main or attached).
2956    pub(crate) fn with_database_schema_mut<T>(
2957        &self,
2958        database_id: usize,
2959        f: impl FnOnce(&mut Schema) -> T,
2960    ) -> Result<T> {
2961        match database_id {
2962            crate::MAIN_DB_ID => self.with_schema_mut(f),
2963            crate::TEMP_DB_ID => {
2964                // The temp database is connection-local, no other connection can
2965                // reference its schema, so we can mutate it directly without cloning
2966                // into `database_schemas`.
2967                let temp_db_guard = self.temp.database.read();
2968                let temp_db = temp_db_guard
2969                    .as_ref()
2970                    .expect("temp database should be initialized before schema mutation");
2971                let mut schema_guard = temp_db.db.schema.lock();
2972                let schema = Schema::try_make_mut(&mut schema_guard)?;
2973                let result = f(schema);
2974                self.bump_prepare_context_generation();
2975                Ok(result)
2976            }
2977            _ => {
2978                // For attached databases, update a connection-local copy of the schema.
2979                // We don't update the shared db.schema until after the WAL commit, so
2980                // other connections won't see uncommitted schema changes (which would
2981                // cause SchemaUpdated mismatches).
2982                let mut schemas = self.database_schemas.write();
2983                let schema_arc = schemas.entry(database_id).or_insert_with(|| {
2984                    let attached_dbs = self.attached_databases.read();
2985                    let (db, _pager) = attached_dbs
2986                        .index_to_data
2987                        .get(&database_id)
2988                        .expect("Database ID should be valid");
2989                    let schema = db.schema.lock().clone();
2990                    schema
2991                });
2992                let schema = Schema::try_make_mut(schema_arc)?;
2993                let result = f(schema);
2994                self.bump_prepare_context_generation();
2995                Ok(result)
2996            }
2997        }
2998    }
2999
3000    pub fn is_db_initialized(&self) -> bool {
3001        self.db.initialized()
3002    }
3003
3004    pub(crate) fn get_pager_from_database_index(&self, index: &usize) -> Result<Arc<Pager>> {
3005        match *index {
3006            crate::MAIN_DB_ID => Ok(self.pager.load().clone()),
3007            crate::TEMP_DB_ID => {
3008                // Lazily initialize the temp database if it hasn't been created yet.
3009                if self.temp.database.read().is_none() {
3010                    self.ensure_temp_database()?;
3011                }
3012                Ok(self
3013                    .temp
3014                    .database
3015                    .read()
3016                    .as_ref()
3017                    .map(|temp_db| temp_db.pager.clone())
3018                    .expect("temp database should be initialized after ensure_temp_database"))
3019            }
3020            _ => Ok(self.attached_databases.read().get_pager_by_index(index)),
3021        }
3022    }
3023
3024    /// Get the database name for a given database index.
3025    /// Returns "main" for index 0, "temp" for index 1, and the alias for attached databases.
3026    pub(crate) fn get_database_name_by_index(&self, index: usize) -> Option<String> {
3027        match index {
3028            MAIN_DB_ID => Some("main".to_string()),
3029            TEMP_DB_ID => Some("temp".to_string()),
3030            _ => self.attached_databases.read().get_name_by_index(index),
3031        }
3032    }
3033
3034    /// Get the database id for a schema name ("main", "temp", or an attached db alias).
3035    pub(crate) fn get_database_id_by_name(&self, name: &str) -> Result<usize> {
3036        let normalized: String = crate::util::normalize_ident(name);
3037        match normalized.as_str() {
3038            "main" => Ok(MAIN_DB_ID),
3039            "temp" => Ok(TEMP_DB_ID),
3040            _ => self
3041                .attached_databases
3042                .read()
3043                .get_database_by_name(&normalized)
3044                .map(|(idx, _)| idx)
3045                .ok_or_else(|| LimboError::InvalidArgument(format!("no such database: {name}"))),
3046        }
3047    }
3048
3049    /// Get the Database object for a given database id.
3050    pub(crate) fn get_source_database(&self, database_id: usize) -> Arc<Database> {
3051        match database_id {
3052            MAIN_DB_ID => self.db.clone(),
3053            TEMP_DB_ID => self
3054                .temp
3055                .database
3056                .read()
3057                .as_ref()
3058                .map(|temp_db| temp_db.db.clone())
3059                .unwrap_or_else(|| self.db.clone()),
3060            _ => self
3061                .attached_databases
3062                .read()
3063                .get_database_by_index(database_id)
3064                .expect("database index should be valid"),
3065        }
3066    }
3067
3068    fn is_attached(&self, alias: &str) -> bool {
3069        self.attached_databases
3070            .read()
3071            .name_to_index
3072            .contains_key(alias)
3073    }
3074
3075    /// Returns the reserved-space value inherited from the main connection's pager.
3076    /// (This reads the main database pager, not the pager of db to be attached)
3077    fn inherited_reserved_space_for_fresh_attach(&self) -> u8 {
3078        let pager = self.pager.load();
3079        pager
3080            .get_reserved_space()
3081            .unwrap_or_else(|| pager.io_ctx.read().get_reserved_space_bytes())
3082    }
3083
3084    /// Returns the minimum reserved space required by the attached pager's own IO context.
3085    /// This is used as a floor so inherited or explicit values cannot undercut the attached DB.
3086    fn minimum_reserved_space_for_fresh_attach(pager: &Pager) -> u8 {
3087        pager
3088            .get_reserved_space()
3089            .unwrap_or(0)
3090            .max(pager.io_ctx.read().get_reserved_space_bytes())
3091    }
3092
3093    fn database_has_existing_wal_state(db: &Database) -> bool {
3094        let shared_wal = db.shared_wal.read();
3095        shared_wal.page_size() != 0 || shared_wal.last_checksum_and_max_frame().1 != 0
3096    }
3097
3098    fn install_database_wal_on_pager(db: &Arc<Database>, pager: &mut Arc<Pager>) {
3099        let shared_wal = db.shared_wal.clone();
3100        let last_checksum_and_max_frame = shared_wal.read().last_checksum_and_max_frame();
3101        let wal = Arc::new(crate::storage::wal::WalFile::new(
3102            db.io.clone(),
3103            shared_wal,
3104            last_checksum_and_max_frame,
3105            db.buffer_pool.clone(),
3106        ));
3107
3108        let pager = Arc::get_mut(pager)
3109            .expect("fresh attached pager must not be shared before bootstrap or publication");
3110        pager.set_wal(wal);
3111    }
3112
3113    fn set_mvcc_journal_mode_fresh_db(pager: &Pager) -> Result<()> {
3114        turso_assert!(!pager.db_initialized());
3115        pager.set_initial_journal_version(crate::storage::sqlite3_ondisk::Version::Mvcc)
3116    }
3117
3118    fn validate_attach_target(db: &Database, is_fresh: bool, alias: &str) -> Result<()> {
3119        if is_fresh && Self::database_has_existing_wal_state(db) {
3120            return Err(LimboError::InvalidArgument(format!(
3121                "cannot attach database '{alias}': main database file is uninitialized but WAL state exists"
3122            )));
3123        }
3124
3125        if is_fresh && db.is_readonly() {
3126            return Err(LimboError::InvalidArgument(format!(
3127                "cannot attach database '{alias}': fresh read-only databases cannot be initialized during attach"
3128            )));
3129        }
3130        Ok(())
3131    }
3132
3133    fn apply_page_layout_to_fresh_attach_db(
3134        &self,
3135        alias: &str,
3136        attached_db_pager: &Pager,
3137        reserved_space: Option<u8>,
3138    ) -> Result<()> {
3139        let target_page_size = self.get_page_size();
3140        let attached_min_reserved_space =
3141            Self::minimum_reserved_space_for_fresh_attach(attached_db_pager);
3142        let target_reserved_space = match reserved_space {
3143            Some(space) => {
3144                // this happens reserved_space is explicitly passed along with encryption or checksum
3145                if space < attached_min_reserved_space {
3146                    return Err(LimboError::InvalidArgument(format!(
3147                        "cannot attach database '{alias}': reserved space {space} is smaller than attached database minimum {attached_min_reserved_space}"
3148                    )));
3149                }
3150                Some(space)
3151            }
3152            None => Some(
3153                self.inherited_reserved_space_for_fresh_attach()
3154                    .max(attached_min_reserved_space),
3155            ),
3156        };
3157
3158        attached_db_pager.set_initial_page_size(target_page_size)?;
3159        if let Some(reserved_space) = target_reserved_space {
3160            attached_db_pager.set_reserved_space_bytes(reserved_space);
3161        }
3162        Ok(())
3163    }
3164
3165    fn reject_initialized_attach_mismatches(
3166        &self,
3167        alias: &str,
3168        db: &Database,
3169        pager: &Pager,
3170    ) -> Result<()> {
3171        // Reject incompatible journal modes for initialized attached databases:
3172        // we cannot silently convert the header (the user may have attached read-only).
3173        if self.mvcc_enabled() != db.mvcc_enabled() {
3174            let main_mode = if self.mvcc_enabled() { "MVCC" } else { "WAL" };
3175            let attached_mode = if db.mvcc_enabled() { "MVCC" } else { "WAL" };
3176            return Err(LimboError::InvalidArgument(format!(
3177                "cannot attach database '{alias}': main database uses {main_mode} journal mode \
3178                 but attached database uses {attached_mode}. Both must use the same journal mode."
3179            )));
3180        }
3181
3182        // Reject mismatched page sizes: ephemeral tables and cross-database
3183        // operations assume a uniform page size across all attached databases.
3184        let main_pager = self.pager.load();
3185        if let (Some(main_ps), Some(attached_ps)) =
3186            (main_pager.get_page_size(), pager.get_page_size())
3187        {
3188            if main_ps != attached_ps {
3189                return Err(LimboError::InvalidArgument(format!(
3190                    "cannot attach database '{alias}': page size mismatch \
3191                     (main={main_ps:?}, attached={attached_ps:?})"
3192                )));
3193            }
3194        }
3195
3196        Ok(())
3197    }
3198
3199    fn reject_unsupported_fresh_mvcc_attach_durable_storage(
3200        &self,
3201        alias: &str,
3202        db: &Database,
3203        attached_is_fresh: bool,
3204    ) -> Result<()> {
3205        if attached_is_fresh
3206            && self.mvcc_enabled()
3207            && self.db.durable_storage.is_some()
3208            && db.durable_storage.is_none()
3209        {
3210            return Err(LimboError::InvalidArgument(format!(
3211                "cannot attach database '{alias}': fresh MVCC attach does not support inheriting custom durable storage"
3212            )));
3213        }
3214
3215        Ok(())
3216    }
3217
3218    /// Attach a database file with the given alias name
3219    #[cfg(not(clt_turso_feature = "fs"))]
3220    pub(crate) fn attach_database(
3221        &self,
3222        _path: &str,
3223        _alias: &str,
3224        _state: &mut AttachDatabaseState,
3225    ) -> Result<IOResult<()>> {
3226        Err(LimboError::InvalidArgument(
3227            "attach not available in this build (no-fs)".to_string(),
3228        ))
3229    }
3230
3231    #[cfg(not(clt_turso_feature = "fs"))]
3232    pub(crate) fn attach_database_with_config(
3233        &self,
3234        _path: &str,
3235        _alias: &str,
3236        _reserved_space: Option<u8>,
3237        _state: &mut AttachDatabaseState,
3238    ) -> Result<IOResult<()>> {
3239        // File-backed ATTACH is unavailable without `fs`, so pre-initialization
3240        // page-layout overrides are also unsupported in this build.
3241        self.attach_database(_path, _alias, _state)
3242    }
3243
3244    /// Attach a database file with the given alias name
3245    #[cfg(clt_turso_feature = "fs")]
3246    pub(crate) fn attach_database(
3247        &self,
3248        path: &str,
3249        alias: &str,
3250        state: &mut AttachDatabaseState,
3251    ) -> Result<IOResult<()>> {
3252        self.attach_database_with_config(path, alias, None, state)
3253    }
3254
3255    /// Attach a database file with an optional pre-initialization reserved-space override.
3256    #[cfg(clt_turso_feature = "fs")]
3257    #[cfg_attr(not(clt_turso_tests), allow(dead_code))]
3258    pub(crate) fn attach_database_with_config(
3259        &self,
3260        path: &str,
3261        alias: &str,
3262        reserved_space: Option<u8>,
3263        state: &mut AttachDatabaseState,
3264    ) -> Result<IOResult<()>> {
3265        loop {
3266            match state {
3267                AttachDatabaseState::Start => {
3268                    if self.is_closed() {
3269                        return Err(LimboError::InternalError("Connection closed".to_string()));
3270                    }
3271
3272                    if self.is_attached(alias) {
3273                        return Err(LimboError::InvalidArgument(format!(
3274                            "database {alias} is already in use"
3275                        )));
3276                    }
3277
3278                    if alias.eq_ignore_ascii_case("main") || alias.eq_ignore_ascii_case("temp") {
3279                        return Err(LimboError::InvalidArgument(format!(
3280                            "reserved name {alias} is already in use"
3281                        )));
3282                    }
3283
3284                    let db_opts = DatabaseOpts::new()
3285                        .with_views(self.db.experimental_views_enabled())
3286                        .with_custom_types(self.db.experimental_custom_types_enabled())
3287                        .with_index_method(self.db.experimental_index_method_enabled())
3288                        .with_vacuum(self.db.experimental_vacuum_enabled())
3289                        .with_generated_columns(self.db.experimental_generated_columns_enabled())
3290                        .with_without_rowid(self.db.experimental_without_rowid_enabled());
3291                    let is_memory_db = is_memory_like(path);
3292                    let io: Arc<dyn IO> = if is_memory_db {
3293                        Arc::new(MemoryIO::new())
3294                    } else if self.db.is_in_memory_db() {
3295                        Database::io_for_path(path)?
3296                    } else {
3297                        self.db.io.clone()
3298                    };
3299                    let main_db_flags = self.db.open_flags;
3300                    let (db, encryption_opts) =
3301                        Self::from_uri_attached(path, db_opts, main_db_flags, io)?;
3302                    let attached_is_fresh = !db.initialized();
3303                    if !is_memory_db {
3304                        Self::validate_attach_target(&db, attached_is_fresh, alias)?;
3305                    }
3306                    self.reject_unsupported_fresh_mvcc_attach_durable_storage(
3307                        alias,
3308                        &db,
3309                        attached_is_fresh,
3310                    )?;
3311
3312                    let encryption_key = if let Some(ref enc) = encryption_opts {
3313                        Some(EncryptionKey::from_hex_string(&enc.hexkey)?)
3314                    } else {
3315                        None
3316                    };
3317
3318                    *state = AttachDatabaseState::Init(Box::new(AttachDatabaseInitState {
3319                        alias: alias.to_string(),
3320                        reserved_space,
3321                        db,
3322                        attached_is_fresh,
3323                        encryption_key,
3324                        init_st: crate::InitState::default(),
3325                    }));
3326                }
3327                AttachDatabaseState::Init(init) => {
3328                    let mut pager = Arc::new(crate::return_if_io!(init
3329                        .db
3330                        ._init_nonblock(&mut init.init_st, init.encryption_key.as_ref(),)));
3331
3332                    if !init.attached_is_fresh {
3333                        self.reject_initialized_attach_mismatches(&init.alias, &init.db, &pager)?;
3334                        *state = AttachDatabaseState::Publish {
3335                            alias: init.alias.clone(),
3336                            db: init.db.clone(),
3337                            pager,
3338                        };
3339                        continue;
3340                    }
3341
3342                    self.apply_page_layout_to_fresh_attach_db(
3343                        &init.alias,
3344                        &pager,
3345                        init.reserved_space,
3346                    )?;
3347
3348                    if self.mvcc_enabled() && !init.db.mvcc_enabled() {
3349                        Self::set_mvcc_journal_mode_fresh_db(&pager)?;
3350                        Self::install_database_wal_on_pager(&init.db, &mut pager);
3351                        let enc_ctx = pager.io_ctx.read().encryption_context().cloned();
3352                        let mv_store = journal_mode::open_mv_store(
3353                            init.db.io.clone(),
3354                            &init.db.path,
3355                            init.db.open_flags,
3356                            init.db.durable_storage.clone(),
3357                            enc_ctx,
3358                            init.db.mv_store_allocator.clone(),
3359                            init.db.experimental_mvcc_passive_checkpoint_enabled(),
3360                        )?;
3361                        init.db.mv_store.store(Some(mv_store));
3362                        *state = AttachDatabaseState::Bootstrap(Box::new(
3363                            AttachDatabaseBootstrapState {
3364                                alias: init.alias.clone(),
3365                                db: init.db.clone(),
3366                                pager,
3367                                encryption_key: init.encryption_key.take(),
3368                                bootstrap_conn: None,
3369                                bootstrap_st: crate::mvcc::database::BootstrapState::default(),
3370                            },
3371                        ));
3372                    } else {
3373                        *state = AttachDatabaseState::Publish {
3374                            alias: init.alias.clone(),
3375                            db: init.db.clone(),
3376                            pager,
3377                        };
3378                    }
3379                }
3380                AttachDatabaseState::Bootstrap(bootstrap) => {
3381                    if bootstrap.bootstrap_conn.is_none() {
3382                        let default_cache_size = match bootstrap
3383                            .pager
3384                            .with_header(|header| header.default_page_cache_size)
3385                        {
3386                            Ok(IOResult::Done(default_cache_size)) => default_cache_size.get(),
3387                            Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
3388                            Err(_) => 0,
3389                        };
3390                        bootstrap.bootstrap_conn =
3391                            Some(bootstrap.db._connect_with_pager_and_default_cache_size(
3392                                true,
3393                                bootstrap.pager.clone(),
3394                                bootstrap.encryption_key.take(),
3395                                default_cache_size,
3396                            )?);
3397                    }
3398
3399                    let mv_store_guard = bootstrap.db.get_mv_store();
3400                    let Some(mv_store) = mv_store_guard.as_ref() else {
3401                        return Err(LimboError::InternalError(
3402                            "fresh MVCC attach missing MV store".to_string(),
3403                        ));
3404                    };
3405                    crate::return_if_io!(mv_store.bootstrap_nonblock(
3406                        bootstrap
3407                            .bootstrap_conn
3408                            .as_ref()
3409                            .expect("bootstrap connection initialized above"),
3410                        &mut bootstrap.bootstrap_st,
3411                    ));
3412
3413                    *state = AttachDatabaseState::Publish {
3414                        alias: bootstrap.alias.clone(),
3415                        db: bootstrap.db.clone(),
3416                        pager: bootstrap.pager.clone(),
3417                    };
3418                }
3419                AttachDatabaseState::Publish { alias, db, pager } => {
3420                    self.attached_databases
3421                        .write()
3422                        .insert(alias.as_str(), (db.clone(), pager.clone()));
3423                    self.bump_prepare_context_generation();
3424                    *state = AttachDatabaseState::Done;
3425                    return Ok(IOResult::Done(()));
3426                }
3427                AttachDatabaseState::Done => {
3428                    return Err(LimboError::InternalError(
3429                        "attach_database called after completion".to_string(),
3430                    ));
3431                }
3432            }
3433        }
3434    }
3435
3436    // Detach a database by alias name
3437    pub(crate) fn detach_database(&self, alias: &str) -> Result<()> {
3438        if self.is_closed() {
3439            return Err(LimboError::InternalError("Connection closed".to_string()));
3440        }
3441
3442        if alias == "main" || alias == "temp" {
3443            return Err(LimboError::InvalidArgument(format!(
3444                "cannot detach database: {alias}"
3445            )));
3446        }
3447
3448        // Look up the database index first, then rollback any MVCC transaction
3449        // *before* removing the database from the catalog.  mv_store_for_db
3450        // and get_pager_from_database_index read `attached_databases`, so we
3451        // must not hold the write lock during the rollback.
3452        let database_id = {
3453            let attached_dbs = self.attached_databases.read();
3454            match attached_dbs.name_to_index.get(alias).copied() {
3455                Some(id) => id,
3456                None => {
3457                    return Err(LimboError::InvalidArgument(format!(
3458                        "no such database: {alias}"
3459                    )));
3460                }
3461            }
3462        };
3463
3464        // Rollback any active transaction on this database before detaching.
3465        // After the Database is removed from the catalog, the MvStore / Pager
3466        // become unreachable and the transaction would leak forever.
3467        let pager = self
3468            .get_pager_from_database_index(&database_id)
3469            .expect("attached database should always have a pager");
3470
3471        if pager.holds_read_lock() || pager.holds_write_lock() {
3472            return Err(LimboError::InvalidArgument(format!(
3473                "database {alias} is locked"
3474            )));
3475        }
3476
3477        if let Some((tx_id, _mode)) = self.get_mv_tx_for_db(database_id) {
3478            if let Some(mv_store) = self.mv_store_for_db(database_id) {
3479                mv_store.rollback_tx(tx_id, pager.clone(), self, database_id);
3480                pager.end_read_tx();
3481            }
3482            self.set_mv_tx_for_db(database_id, None);
3483        } else {
3484            // Non-MVCC attached DB (e.g. :memory:) — rollback WAL state.
3485            pager.rollback_attached();
3486        }
3487
3488        // Remove from catalog. The write lock must be released before
3489        // acquiring database_schemas.write() to maintain consistent lock
3490        // ordering (attached_databases before database_schemas).
3491        {
3492            let mut attached_dbs = self.attached_databases.write();
3493            attached_dbs.remove(alias);
3494        }
3495
3496        // Invalidate the cached schema for this database index so that a future
3497        // ATTACH reusing the same index won't see stale schema entries.
3498        self.database_schemas.write().remove(&database_id);
3499        self.bump_prepare_context_generation();
3500
3501        Ok(())
3502    }
3503
3504    /// List all attached database aliases
3505    pub fn list_attached_databases(&self) -> Vec<String> {
3506        self.attached_databases
3507            .read()
3508            .name_to_index
3509            .keys()
3510            .cloned()
3511            .collect()
3512    }
3513
3514    /// Invoke `f` with a slice of all non-main database (index, pager) pairs
3515    /// (temp + attached).The internal locks are released before `f` runs, which also
3516    /// makes it safe for `f` to call back into the connection (e.g. `mv_store_for_db`,
3517    /// which re-reads the attached-database catalog).
3518    pub(crate) fn with_all_attached_pagers_with_index<F, R>(&self, f: F) -> R
3519    where
3520        F: FnOnce(&[(usize, Arc<Pager>)]) -> R,
3521    {
3522        let mut pagers: SmallVec<[(usize, Arc<Pager>); 8]> = SmallVec::new();
3523        if let Some(temp_db) = self.temp.database.read().as_ref() {
3524            pagers.push((crate::TEMP_DB_ID, temp_db.pager.clone()));
3525        }
3526        {
3527            let catalog = self.attached_databases.read();
3528            for (&idx, (_db, pager)) in catalog.index_to_data.iter() {
3529                pagers.push((idx, pager.clone()));
3530            }
3531        }
3532        f(&pagers)
3533    }
3534
3535    pub(crate) fn database_schemas(&self) -> &RwLock<HashMap<usize, Arc<Schema>>> {
3536        &self.database_schemas
3537    }
3538
3539    fn cached_non_main_schema(&self, database_id: usize) -> Arc<Schema> {
3540        turso_assert_ne!(database_id, crate::MAIN_DB_ID);
3541        // TEMP is the sole source-of-truth path: writes go directly to
3542        // `temp_db.db.schema` (see `with_database_schema_mut`), so skip
3543        // `database_schemas` entirely to avoid stale reads.
3544        if database_id == crate::TEMP_DB_ID {
3545            return self
3546                .temp
3547                .database
3548                .read()
3549                .as_ref()
3550                .map(|temp_db| temp_db.db.schema.lock().clone())
3551                .unwrap_or_else(|| self.empty_temp_schema());
3552        }
3553        if let Some(schema) = self.database_schemas.read().get(&database_id).cloned() {
3554            return schema;
3555        }
3556
3557        let attached_dbs = self.attached_databases.read();
3558        let (db, _pager) = attached_dbs
3559            .index_to_data
3560            .get(&database_id)
3561            .expect("Database ID should be valid after resolve_database_id");
3562        let schema = db.schema.lock().clone();
3563        schema
3564    }
3565
3566    /// Publish a connection-local non-main schema after commit.
3567    ///
3568    /// TEMP is not staged in `database_schemas` — writes go directly to
3569    /// `temp_db.db.schema` via `with_database_schema_mut`, so there is
3570    /// nothing to publish here. Attached databases still stage mutations
3571    /// in `database_schemas` so other connections don't see uncommitted
3572    /// DDL; those get published to the shared `db.schema` on commit.
3573    pub(crate) fn publish_database_schema(&self, database_id: usize) {
3574        if database_id == crate::TEMP_DB_ID {
3575            return;
3576        }
3577        let mut schemas = self.database_schemas.write();
3578        if let Some(local_schema) = schemas.remove(&database_id) {
3579            let attached_dbs = self.attached_databases.read();
3580            if let Some((db, _pager)) = attached_dbs.index_to_data.get(&database_id) {
3581                *db.schema.lock() = local_schema;
3582            }
3583            self.bump_prepare_context_generation();
3584        }
3585    }
3586
3587    pub(crate) fn attached_databases(&self) -> &RwLock<DatabaseCatalog> {
3588        &self.attached_databases
3589    }
3590
3591    /// Access schema for a database using a closure pattern to avoid cloning
3592    pub(crate) fn with_schema<T>(&self, database_id: usize, f: impl FnOnce(&Schema) -> T) -> T {
3593        match database_id {
3594            crate::MAIN_DB_ID => {
3595                let schema = self.schema.read();
3596                f(&schema)
3597            }
3598            _ => {
3599                let schema = self.cached_non_main_schema(database_id);
3600                f(&schema)
3601            }
3602        }
3603    }
3604
3605    /// Clone the *shared* schema of `database_id` (main or attached), bypassing
3606    /// the per-connection schema cache. Falls back to the main DB's shared
3607    /// schema when `database_id` does not name an attached database — callers
3608    /// in error paths get something usable instead of a panic.
3609    ///
3610    /// MVCC checkpoint specifically must call this rather than [`Self::with_schema`]:
3611    /// it writes from the mv store to the pager, so the schema it uses must
3612    /// match the pager being checkpointed and cannot be a stale per-connection
3613    /// copy.
3614    pub(crate) fn clone_shared_schema(&self, database_id: usize) -> Arc<Schema> {
3615        if database_id == crate::MAIN_DB_ID {
3616            self.db.clone_schema()
3617        } else {
3618            self.attached_databases
3619                .read()
3620                .index_to_data
3621                .get(&database_id)
3622                .map(|(db, _)| db.schema.lock().clone())
3623                .unwrap_or_else(|| self.db.clone_schema())
3624        }
3625    }
3626
3627    // Get the canonical path for a database given its Database object
3628    fn get_canonical_path_for_database(db: &Database) -> String {
3629        if db.is_in_memory_db() {
3630            // For in-memory databases, SQLite shows empty string
3631            String::new()
3632        } else {
3633            // For file databases, try to show the full absolute path if that doesn't fail
3634            match std::fs::canonicalize(&db.path) {
3635                Ok(abs_path) => abs_path.to_string_lossy().to_string(),
3636                Err(_) => db.path.to_string(),
3637            }
3638        }
3639    }
3640
3641    /// List all databases (main + attached) with their sequence numbers, names, and file paths
3642    /// Returns a vector of tuples: (seq_number, name, file_path)
3643    pub fn list_all_databases(&self) -> Vec<(usize, String, String)> {
3644        let mut databases = Vec::new();
3645
3646        // Add main database (always seq=0, name="main")
3647        let main_path = Self::get_canonical_path_for_database(&self.db);
3648        databases.push((MAIN_DB_ID, "main".to_string(), main_path));
3649
3650        // SQLite only exposes the temp schema in database_list after it has
3651        // been initialized, and reports an empty path rather than the backing
3652        // temp filename.
3653        if self.temp.database.read().is_some() {
3654            databases.push((crate::TEMP_DB_ID, "temp".to_string(), String::new()));
3655        }
3656
3657        // Add attached databases
3658        let attached_dbs = self.attached_databases.read();
3659        for (alias, &seq_number) in attached_dbs.name_to_index.iter() {
3660            let file_path = if let Some((db, _pager)) = attached_dbs.index_to_data.get(&seq_number)
3661            {
3662                Self::get_canonical_path_for_database(db)
3663            } else {
3664                String::new()
3665            };
3666            databases.push((seq_number, alias.clone(), file_path));
3667        }
3668
3669        // Sort by sequence number to ensure consistent ordering
3670        databases.sort_by_key(|&(seq, _, _)| seq);
3671        databases
3672    }
3673
3674    pub fn get_pager(&self) -> Arc<Pager> {
3675        self.pager.load().clone()
3676    }
3677
3678    pub fn get_query_only(&self) -> bool {
3679        self.is_query_only()
3680    }
3681
3682    pub fn set_query_only(&self, value: bool) {
3683        self.query_only.store(value, Ordering::SeqCst);
3684        self.bump_prepare_context_generation();
3685    }
3686
3687    pub fn set_vdbe_trace(&self, value: bool) {
3688        self.vdbe_trace.store(value, Ordering::SeqCst);
3689    }
3690
3691    pub fn get_vdbe_trace(&self) -> bool {
3692        self.vdbe_trace.load(Ordering::SeqCst)
3693    }
3694
3695    pub fn get_dml_require_where(&self) -> bool {
3696        self.dml_require_where.load(Ordering::SeqCst)
3697    }
3698
3699    pub fn set_dml_require_where(&self, value: bool) {
3700        self.dml_require_where.store(value, Ordering::SeqCst);
3701    }
3702
3703    pub fn get_dqs_dml(&self) -> bool {
3704        self.dqs_dml.load(Ordering::SeqCst)
3705    }
3706
3707    pub fn set_dqs_dml(&self, value: bool) {
3708        self.dqs_dml.store(value, Ordering::SeqCst);
3709        self.bump_prepare_context_generation();
3710    }
3711
3712    pub fn get_full_column_names(&self) -> bool {
3713        self.full_column_names.load(Ordering::SeqCst)
3714    }
3715
3716    pub fn set_full_column_names(&self, value: bool) {
3717        self.full_column_names.store(value, Ordering::SeqCst);
3718        self.bump_prepare_context_generation();
3719    }
3720
3721    pub fn get_short_column_names(&self) -> bool {
3722        self.short_column_names.load(Ordering::SeqCst)
3723    }
3724
3725    pub fn set_short_column_names(&self, value: bool) {
3726        self.short_column_names.store(value, Ordering::SeqCst);
3727        self.bump_prepare_context_generation();
3728    }
3729
3730    pub fn get_sync_mode(&self) -> SyncMode {
3731        self.sync_mode.get()
3732    }
3733
3734    pub fn set_sync_mode(&self, mode: SyncMode) {
3735        self.sync_mode.set(mode);
3736        self.bump_prepare_context_generation();
3737    }
3738
3739    pub fn get_temp_store(&self) -> crate::TempStore {
3740        self.temp_store.get()
3741    }
3742
3743    pub fn set_temp_store(&self, value: crate::TempStore) {
3744        if self.temp_store.get() == value {
3745            return;
3746        }
3747        self.reset_temp_database();
3748        self.temp_store.set(value);
3749        self.bump_prepare_context_generation();
3750    }
3751
3752    /// Find a sequence by name, supporting optional schema qualification.
3753    ///
3754    /// - `"my_seq"` → searches main database only
3755    /// - `"aux.my_seq"` → searches the attached database named `aux`
3756    pub fn find_sequence(&self, name: &str) -> Result<Arc<crate::schema::Sequence>> {
3757        let (db_id, seq_name) = if let Some((schema, seq)) = name.split_once('.') {
3758            let db_id = self.get_database_id_by_name(schema)?;
3759            (db_id, crate::util::normalize_ident(seq))
3760        } else {
3761            (MAIN_DB_ID, crate::util::normalize_ident(name))
3762        };
3763
3764        self.with_schema(db_id, |schema| {
3765            schema.get_sequence(&seq_name).map(Arc::clone)
3766        })
3767        .ok_or_else(|| LimboError::ParseError(format!("sequence \"{name}\" does not exist")))
3768    }
3769
3770    /// Record that this connection has seen a value from the named sequence (for currval).
3771    pub fn set_sequence_currval(&self, name: &str, value: i64) {
3772        let normalized = crate::util::normalize_ident(name);
3773        self.sequence_currvals.write().insert(normalized, value);
3774    }
3775
3776    /// Get the last value returned by nextval/setval for the named sequence on this connection.
3777    pub fn get_sequence_currval(&self, name: &str) -> Option<i64> {
3778        let normalized = crate::util::normalize_ident(name);
3779        self.sequence_currvals.read().get(&normalized).copied()
3780    }
3781
3782    /// Drop this connection's currval entry for a sequence. Called on DROP
3783    /// SEQUENCE (and implicit drops via DROP TABLE on AUTOINCREMENT) so that
3784    /// a subsequent `CREATE SEQUENCE <same-name>` does not silently inherit
3785    /// the stale per-session currval from the prior sequence — `currval()`
3786    /// on the fresh sequence must error with "not yet defined in this
3787    /// session" until a nextval/setval establishes it.
3788    pub fn clear_sequence_currval(&self, name: &str) {
3789        let normalized = crate::util::normalize_ident(name);
3790        self.sequence_currvals.write().remove(&normalized);
3791    }
3792
3793    /// Total times this connection's autonomous sequence inner-tx ran into
3794    /// a transient conflict (`WriteWriteConflict` / `BusySnapshot` /
3795    /// `Conflict(_)`) and was retried by `op_sequence_commit_inner_tx`.
3796    /// A non-CYCLE nextval on a non-contended seq must keep this at zero —
3797    /// the regression test for "no inline backing-table compaction"
3798    /// asserts the delta is 0 across the concurrent-nextval scenario.
3799    pub fn sequence_inner_retries(&self) -> u64 {
3800        self.sequence_inner_retries
3801            .load(std::sync::atomic::Ordering::Relaxed)
3802    }
3803
3804    /// Reset the inner-tx retry counter. Test-only helper so a setup
3805    /// phase (priming the backing table, etc.) doesn't pollute the
3806    /// counter the assertion phase observes.
3807    #[doc(hidden)]
3808    pub fn reset_sequence_inner_retries(&self) {
3809        self.sequence_inner_retries
3810            .store(0, std::sync::atomic::Ordering::Relaxed);
3811    }
3812
3813    /// Bootstrap-time sequence descriptor loader. Used by MVCC bootstrap
3814    /// after log recovery: walks `__turso_internal_seq_*` tables and registers
3815    /// a pure descriptor for each into the active schema. No atomic state is
3816    /// seeded — the runtime watermark is always read from disk by
3817    /// nextval/setval.
3818    ///
3819    /// Non-blocking: driven by the bootstrap state machine via `return_if_io!`,
3820    /// so the per-backing-table descriptor read yields IO rather than pumping
3821    /// `io.step()`. Re-entrant — the worklist and in-flight read live in
3822    /// `state`.
3823    pub(crate) fn load_sequence_descriptors_via_sql_nonblock(
3824        self: &Arc<Connection>,
3825        state: &mut LoadSequenceDescriptorsState,
3826    ) -> Result<crate::types::IOResult<()>> {
3827        use crate::types::IOResult;
3828        loop {
3829            match state {
3830                LoadSequenceDescriptorsState::Start => {
3831                    // Walk schema.tables in-memory rather than issuing a SELECT
3832                    // against sqlite_master — avoids the side-effects of running
3833                    // a fresh statement here, which can leave the connection's
3834                    // mv_tx in a non-exclusive state and cause the next DDL to
3835                    // trip the exclusive-tx guard in op_open_write.
3836                    let pending =
3837                        self.with_schema(MAIN_DB_ID, |s| s.sequence_backing_table_names());
3838                    *state = LoadSequenceDescriptorsState::Reading {
3839                        pending,
3840                        idx: 0,
3841                        stmt: None,
3842                        meta: None,
3843                        seq: None,
3844                        watermark_stmt: None,
3845                        watermark_row: None,
3846                    };
3847                }
3848                LoadSequenceDescriptorsState::Reading {
3849                    pending,
3850                    idx,
3851                    stmt,
3852                    meta,
3853                    seq,
3854                    watermark_stmt,
3855                    watermark_row,
3856                } => loop {
3857                    let entry = {
3858                        if *idx >= pending.len() {
3859                            return Ok(IOResult::Done(()));
3860                        }
3861                        pending[*idx].clone()
3862                    };
3863                    let (backing_table_name, seq_name) = entry;
3864                    let normalized = crate::util::normalize_ident(&seq_name);
3865                    let already_present =
3866                        self.with_schema(MAIN_DB_ID, |s| s.get_sequence(&normalized).is_some());
3867                    if already_present {
3868                        *idx += 1;
3869                        *stmt = None;
3870                        *meta = None;
3871                        *seq = None;
3872                        *watermark_stmt = None;
3873                        *watermark_row = None;
3874                        continue;
3875                    }
3876                    if seq.is_none() {
3877                        crate::return_if_io!(self.read_seq_descriptor_row_nonblock(
3878                            &backing_table_name,
3879                            &seq_name,
3880                            stmt,
3881                            meta,
3882                        ));
3883                        *seq = Some(Self::sequence_from_descriptor_meta(
3884                            &seq_name,
3885                            &backing_table_name,
3886                            *meta,
3887                        )?);
3888                        *stmt = None;
3889                        *meta = None;
3890                    }
3891                    let sequence = seq.as_ref().expect("sequence set above");
3892                    crate::return_if_io!(self.read_sequence_watermark_row_nonblock(
3893                        &backing_table_name,
3894                        sequence,
3895                        watermark_stmt,
3896                        watermark_row,
3897                    ));
3898                    let watermark = Self::sequence_watermark_from_row(
3899                        &backing_table_name,
3900                        sequence,
3901                        *watermark_row,
3902                    )?;
3903                    if let Some(mv_store) = self.db.get_mv_store().as_ref() {
3904                        mv_store.set_sequence_watermark(&normalized, watermark);
3905                    }
3906                    let sequence = seq.take().expect("sequence set above");
3907                    self.with_database_schema_mut(MAIN_DB_ID, |schema| {
3908                        schema
3909                            .sequences
3910                            .insert(normalized.clone(), Arc::new(sequence));
3911                    })?;
3912                    *idx += 1;
3913                    *stmt = None;
3914                    *meta = None;
3915                    *watermark_stmt = None;
3916                    *watermark_row = None;
3917                },
3918            }
3919        }
3920    }
3921
3922    /// Drive one backing-table descriptor read to completion (re-entrant).
3923    /// Lazily prepares the `SELECT` into `*stmt`, then runs it non-blocking,
3924    /// stashing the captured row in `*meta`. The backing table is internal
3925    /// (`__turso_internal_seq_*`); a prepare/read failure is on-disk
3926    /// corruption, not "the sequence doesn't exist", so it surfaces
3927    /// `LimboError::Corrupt` — silently dropping the sequence would manifest
3928    /// as a misleading "sequence does not exist" error on the next nextval
3929    /// that masks the real problem.
3930    fn read_seq_descriptor_row_nonblock(
3931        self: &Arc<Connection>,
3932        backing_table_name: &str,
3933        seq_name: &str,
3934        stmt: &mut Option<Box<Statement>>,
3935        meta: &mut Option<(i64, i64, i64, i64, bool)>,
3936    ) -> Result<crate::types::IOResult<()>> {
3937        use crate::types::IOResult;
3938        if stmt.is_none() {
3939            let escaped = backing_table_name.replace('"', "\"\"");
3940            let sql = format!("SELECT start, inc, min, max, cycle FROM \"{escaped}\" LIMIT 1");
3941            let prepared = self.prepare_internal(sql).map_err(|err| {
3942                LimboError::Corrupt(format!(
3943                    "internal sequence backing table \"{backing_table_name}\" for sequence \
3944                     \"{seq_name}\": cannot prepare descriptor SELECT: {err}"
3945                ))
3946            })?;
3947            *stmt = Some(Box::new(prepared));
3948            // Fresh statement → clear any descriptor captured for a prior backing
3949            // table, so an empty backing table is detected as missing-row
3950            // corruption rather than silently reusing the previous descriptor.
3951            *meta = None;
3952        }
3953        let s = stmt.as_mut().expect("stmt set above");
3954        match s.run_with_row_callback_nonblock(|row| {
3955            *meta = Some((
3956                row.get::<i64>(0)?,
3957                row.get::<i64>(1)?,
3958                row.get::<i64>(2)?,
3959                row.get::<i64>(3)?,
3960                row.get::<i64>(4)? != 0,
3961            ));
3962            Ok(())
3963        }) {
3964            Ok(IOResult::IO(io)) => Ok(IOResult::IO(io)),
3965            Ok(IOResult::Done(())) => Ok(IOResult::Done(())),
3966            Err(err) => Err(LimboError::Corrupt(format!(
3967                "internal sequence backing table \"{backing_table_name}\" for sequence \
3968                 \"{seq_name}\": descriptor row read failed: {err}"
3969            ))),
3970        }
3971    }
3972
3973    /// Build a `Sequence` from a descriptor row captured by
3974    /// [`Self::read_seq_descriptor_row_nonblock`]. An absent/invalid descriptor
3975    /// is on-disk corruption (see that method's doc).
3976    fn sequence_from_descriptor_meta(
3977        seq_name: &str,
3978        backing_table_name: &str,
3979        meta: Option<(i64, i64, i64, i64, bool)>,
3980    ) -> Result<crate::schema::Sequence> {
3981        let (start, inc, min, max, cycle) = meta.ok_or_else(|| {
3982            LimboError::Corrupt(format!(
3983                "internal sequence backing table \"{backing_table_name}\" for sequence \
3984                 \"{seq_name}\" is empty; the descriptor metadata row must always be present"
3985            ))
3986        })?;
3987        crate::schema::Sequence::new(
3988            seq_name.to_string(),
3989            Some(start),
3990            Some(inc),
3991            Some(min),
3992            Some(max),
3993            cycle,
3994        )
3995        .map_err(|err| {
3996            LimboError::Corrupt(format!(
3997                "internal sequence backing table \"{backing_table_name}\" for sequence \
3998                 \"{seq_name}\" descriptor is invalid: {err}"
3999            ))
4000        })
4001    }
4002
4003    /// Drive one backing-table watermark read to completion (re-entrant).
4004    ///
4005    /// The returned row is converted by [`Self::sequence_watermark_from_row`]
4006    /// into the exclusive upper bound used by `sequence_watermark_experimental()`.
4007    fn read_sequence_watermark_row_nonblock(
4008        self: &Arc<Connection>,
4009        backing_table_name: &str,
4010        seq: &crate::schema::Sequence,
4011        stmt: &mut Option<Box<Statement>>,
4012        row: &mut Option<(i64, bool)>,
4013    ) -> Result<crate::types::IOResult<()>> {
4014        use crate::types::IOResult;
4015        if stmt.is_none() {
4016            let escaped = backing_table_name.replace('"', "\"\"");
4017            let direction = if seq.increment_by >= 0 { "DESC" } else { "ASC" };
4018            let sql = format!(
4019                "SELECT value, is_called FROM \"{escaped}\" ORDER BY value {direction} LIMIT 1"
4020            );
4021            let prepared = self.prepare_internal(sql).map_err(|err| {
4022                LimboError::Corrupt(format!(
4023                    "internal sequence backing table \"{backing_table_name}\" for sequence \
4024                     \"{}\": cannot prepare watermark SELECT: {err}",
4025                    seq.name
4026                ))
4027            })?;
4028            *stmt = Some(Box::new(prepared));
4029            *row = None;
4030        }
4031        let s = stmt.as_mut().expect("stmt set above");
4032        match s.run_with_row_callback_nonblock(|r| {
4033            let value = r.get::<i64>(0)?;
4034            let is_called = r.get::<i64>(1)? != 0;
4035            *row = Some((value, is_called));
4036            Ok(())
4037        }) {
4038            Ok(IOResult::IO(io)) => Ok(IOResult::IO(io)),
4039            Ok(IOResult::Done(())) => Ok(IOResult::Done(())),
4040            Err(err) => Err(LimboError::Corrupt(format!(
4041                "internal sequence backing table \"{backing_table_name}\" for sequence \
4042                 \"{}\": watermark row read failed: {err}",
4043                seq.name
4044            ))),
4045        }
4046    }
4047
4048    fn sequence_watermark_from_row(
4049        backing_table_name: &str,
4050        seq: &crate::schema::Sequence,
4051        row: Option<(i64, bool)>,
4052    ) -> Result<i64> {
4053        let (value, is_called) = row.ok_or_else(|| {
4054            LimboError::Corrupt(format!(
4055                "internal sequence backing table \"{backing_table_name}\" for sequence \
4056                 \"{}\" is empty; cannot derive sequence watermark",
4057                seq.name
4058            ))
4059        })?;
4060        Ok(crate::mvcc::database::first_unsafe_sequence_watermark(
4061            seq, value, is_called,
4062        ))
4063    }
4064
4065    /// Sync AUTOINCREMENT backing-table watermarks from `sqlite_sequence`.
4066    ///
4067    /// Covers the WAL→MVCC mode-switch compatibility path: a WAL-mode
4068    /// database with AUTOINCREMENT tables tracks the high-water mark in
4069    /// `sqlite_sequence` (legacy SQLite contract) and never writes to
4070    /// the backing table created by CREATE TABLE bytecode. The MVCC
4071    /// AUTOINCREMENT path reads the backing table to compute the next
4072    /// rowid, so without a sync step the next INSERT would regress to
4073    /// start_value and collide with the existing rowid.
4074    ///
4075    /// For each `name` in `sqlite_sequence`, locate the backing table
4076    /// `__turso_internal_seq___turso_internal_autoincrement_<name>` and,
4077    /// if its current MAX(value) is below the sqlite_sequence value,
4078    /// INSERT a new watermark row to advance it. This is the same
4079    /// pattern the translator emits for `emit_disk_advance_past`,
4080    /// expressed as statement-level SQL so it can run at bootstrap.
4081    ///
4082    /// Tables whose backing table is missing are skipped — that
4083    /// indicates the table was never an AUTOINCREMENT under Turso's
4084    /// CREATE TABLE bytecode (i.e. it predates this engine touching
4085    /// the DB), and synthesising a backing table here would forge data
4086    /// the user did not author. Importing a foreign SQLite database is
4087    /// out of scope for this helper.
4088    ///
4089    /// Non-blocking: driven by the bootstrap state machine via `return_if_io!`.
4090    /// Each step is on the correctness path documented above — a silent failure
4091    /// leaves MVCC AUTOINCREMENT able to re-emit a rowid already in use after a
4092    /// WAL→MVCC mode switch, so errors propagate to fail the open rather than
4093    /// continue into a state where the next INSERT NULL collides on disk.
4094    pub(crate) fn sync_autoincrement_backing_tables_from_sqlite_sequence_nonblock(
4095        self: &Arc<Connection>,
4096        state: &mut SyncAutoincrementState,
4097    ) -> Result<crate::types::IOResult<()>> {
4098        use crate::schema::{autoincrement_sequence_name, SQLITE_SEQUENCE_TABLE_NAME};
4099        use crate::translate::sequence::sequence_backing_table_name;
4100        use crate::types::IOResult;
4101
4102        loop {
4103            match state {
4104                SyncAutoincrementState::Start => {
4105                    let has_seq_table = self.with_schema(MAIN_DB_ID, |s| {
4106                        s.get_btree_table(SQLITE_SEQUENCE_TABLE_NAME).is_some()
4107                    });
4108                    if !has_seq_table {
4109                        return Ok(IOResult::Done(()));
4110                    }
4111                    let stmt = self.prepare_internal(format!(
4112                        "SELECT name, seq FROM {SQLITE_SEQUENCE_TABLE_NAME}"
4113                    ))?;
4114                    *state = SyncAutoincrementState::ReadSeqRows {
4115                        stmt: Box::new(stmt),
4116                        rows: Vec::new(),
4117                    };
4118                }
4119                SyncAutoincrementState::ReadSeqRows { stmt, rows } => {
4120                    crate::return_if_io!(stmt.run_with_row_callback_nonblock(|row| {
4121                        let name = row.get::<&str>(0)?.to_string();
4122                        let seq = row.get::<i64>(1)?;
4123                        rows.push((name, seq));
4124                        Ok(())
4125                    }));
4126                    let rows = std::mem::take(rows);
4127                    *state = SyncAutoincrementState::Process {
4128                        rows,
4129                        idx: 0,
4130                        sub: SyncRowStep::Start,
4131                    };
4132                }
4133                SyncAutoincrementState::Process { rows, idx, sub } => {
4134                    if *idx >= rows.len() {
4135                        return Ok(IOResult::Done(()));
4136                    }
4137                    match sub {
4138                        SyncRowStep::Start => {
4139                            let backing_table_name = sequence_backing_table_name(
4140                                &autoincrement_sequence_name(&rows[*idx].0),
4141                            );
4142                            let has_backing = self.with_schema(MAIN_DB_ID, |s| {
4143                                s.get_btree_table(&backing_table_name).is_some()
4144                            });
4145                            if !has_backing {
4146                                *idx += 1;
4147                                continue;
4148                            }
4149                            // Read current backing watermark; only upsert if we'd
4150                            // actually advance it (avoids needless writes on boot).
4151                            let escaped = backing_table_name.replace('"', "\"\"");
4152                            let stmt = self.prepare_internal(format!(
4153                                "SELECT MAX(value) FROM \"{escaped}\""
4154                            ))?;
4155                            *sub = SyncRowStep::ReadMax {
4156                                backing_table_name,
4157                                stmt: Box::new(stmt),
4158                                current_max: None,
4159                            };
4160                        }
4161                        SyncRowStep::ReadMax {
4162                            backing_table_name,
4163                            stmt,
4164                            current_max,
4165                        } => {
4166                            crate::return_if_io!(stmt.run_with_row_callback_nonblock(|row| {
4167                                if let crate::Value::Numeric(crate::Numeric::Integer(v)) =
4168                                    row.get_value(0)
4169                                {
4170                                    *current_max = Some(*v);
4171                                }
4172                                Ok(())
4173                            }));
4174                            let watermark = rows[*idx].1;
4175                            // Skip only when the backing table is already strictly
4176                            // ahead; an equal value is NOT enough because the
4177                            // initial row written by CREATE TABLE bytecode is
4178                            // (value=1, is_called=false), which would cause the
4179                            // next nextval to re-emit value=1 and collide with the
4180                            // rowid already inserted in WAL mode. We always upsert
4181                            // with is_called=1 so the next nextval computes
4182                            // watermark+1 like sqlite_sequence semantics demand.
4183                            if matches!(*current_max, Some(c) if c > watermark) {
4184                                *idx += 1;
4185                                *sub = SyncRowStep::Start;
4186                                continue;
4187                            }
4188                            // Standard AUTOINCREMENT descriptor columns (start=1,
4189                            // inc=1, min=1, max=i64::MAX, cycle=0) — mirror what
4190                            // the translator emits when CREATE TABLE bytecode
4191                            // creates the backing table for an AUTOINCREMENT column.
4192                            let escaped = backing_table_name.replace('"', "\"\"");
4193                            let insert_sql = format!(
4194                                "INSERT OR REPLACE INTO \"{escaped}\"\
4195                                 (value, is_called, start, inc, min, max, cycle) \
4196                                 VALUES ({watermark}, 1, 1, 1, 1, {}, 0)",
4197                                i64::MAX
4198                            );
4199                            let stmt = self.prepare_internal(insert_sql)?;
4200                            *sub = SyncRowStep::Upsert {
4201                                stmt: Box::new(stmt),
4202                            };
4203                        }
4204                        SyncRowStep::Upsert { stmt } => {
4205                            crate::return_if_io!(stmt.run_with_row_callback_nonblock(|_| Ok(())));
4206                            if let Some(mv_store) = self.db.get_mv_store().as_ref() {
4207                                let watermark = rows[*idx].1;
4208                                let first_unsafe = watermark.checked_add(1).unwrap_or(watermark);
4209                                mv_store.set_sequence_watermark(
4210                                    &autoincrement_sequence_name(&rows[*idx].0),
4211                                    first_unsafe,
4212                                );
4213                            }
4214                            *idx += 1;
4215                            *sub = SyncRowStep::Start;
4216                        }
4217                    }
4218                }
4219            }
4220        }
4221    }
4222
4223    /// Create a `TempDir` honoring `TURSO_TMPDIR` and `SQLITE_TMPDIR`,
4224    /// falling back to the OS default (`env::temp_dir()`).
4225    ///
4226    /// `&self` is reserved for a future per-connection
4227    /// `temp_store_directory` setting (e.g. `PRAGMA temp_store_directory`)
4228    /// so call sites don't need to change when that lands.
4229    #[cfg(not(target_family = "wasm"))]
4230    pub(crate) fn create_tempdir(&self) -> Result<TempDir> {
4231        let res = if let Some(d) = std::env::var_os("TURSO_TMPDIR") {
4232            tempfile::tempdir_in(d)
4233        } else if let Some(d) = std::env::var_os("SQLITE_TMPDIR") {
4234            tempfile::tempdir_in(d)
4235        } else {
4236            tempfile::tempdir()
4237        };
4238        res.map_err(|e| io_error(e, "tempdir"))
4239    }
4240
4241    pub fn get_data_sync_retry(&self) -> bool {
4242        self.data_sync_retry
4243            .load(crate::sync::atomic::Ordering::SeqCst)
4244    }
4245
4246    pub fn set_data_sync_retry(&self, value: bool) {
4247        self.data_sync_retry
4248            .store(value, crate::sync::atomic::Ordering::SeqCst);
4249        self.bump_prepare_context_generation();
4250    }
4251
4252    /// Get the sync type setting.
4253    pub fn get_sync_type(&self) -> crate::io::FileSyncType {
4254        self.pager.load().get_sync_type()
4255    }
4256
4257    /// Set the sync type (for PRAGMA fullfsync).
4258    pub fn set_sync_type(&self, value: crate::io::FileSyncType) {
4259        self.pager.load().set_sync_type(value);
4260    }
4261
4262    /// Creates a HashSet of modules that have been loaded
4263    pub fn get_syms_vtab_mods(&self) -> HashSet<String> {
4264        self.syms.read().vtab_modules.keys().cloned().collect()
4265    }
4266
4267    /// Returns external (extension) functions: (name, is_aggregate, argc, deterministic)
4268    pub fn get_syms_functions(&self) -> Vec<(String, bool, i32, bool)> {
4269        self.syms
4270            .read()
4271            .functions
4272            .values()
4273            .map(|f| {
4274                let is_agg = f.func.is_aggregate();
4275                let argc = match &f.func {
4276                    function::ExtFunc::Aggregate { argc, .. } => *argc,
4277                    function::ExtFunc::Scalar { argc, .. } => *argc,
4278                };
4279                (
4280                    f.name.clone(),
4281                    is_agg,
4282                    argc,
4283                    function::Deterministic::is_deterministic(f.as_ref()),
4284                )
4285            })
4286            .collect()
4287    }
4288
4289    pub fn register_external_collation(
4290        &self,
4291        name: String,
4292        context: usize,
4293        callback: crate::ContextCollationFunction,
4294        context_destructor: Option<crate::ContextDestructor>,
4295    ) {
4296        let collation = CollationSeq::custom(&name);
4297        let normalized_name = crate::util::normalize_ident(&name);
4298        self.syms.write().collations.insert(
4299            collation.id(),
4300            Arc::new(function::ExternalCollation::new(
4301                normalized_name,
4302                context,
4303                callback,
4304                context_destructor,
4305            )),
4306        );
4307        self.bump_prepare_context_generation();
4308    }
4309
4310    pub fn unregister_external_collation(&self, name: &str) {
4311        if let Some(collation) = CollationSeq::known_custom(name) {
4312            if self
4313                .syms
4314                .write()
4315                .collations
4316                .remove(&collation.id())
4317                .is_some()
4318            {
4319                self.bump_prepare_context_generation();
4320            }
4321        }
4322    }
4323
4324    pub(crate) fn get_external_collation(
4325        &self,
4326        collation: CollationSeq,
4327    ) -> Result<Arc<function::ExternalCollation>> {
4328        self.syms
4329            .read()
4330            .collations
4331            .get(&collation.id())
4332            .cloned()
4333            .ok_or_else(|| {
4334                LimboError::ParseError(format!("no such collation sequence: {}", collation.name()))
4335            })
4336    }
4337
4338    pub(crate) fn custom_collation_compare(
4339        external: &function::ExternalCollation,
4340        left: &str,
4341        right: &str,
4342    ) -> CmpOrdering {
4343        let result = unsafe {
4344            (external.callback)(
4345                external.context,
4346                left.as_ptr(),
4347                left.len(),
4348                right.as_ptr(),
4349                right.len(),
4350            )
4351        };
4352        result.cmp(&0)
4353    }
4354
4355    pub(crate) fn external_collation_comparator(
4356        external: Arc<function::ExternalCollation>,
4357    ) -> crate::vdbe::sorter::SortComparator {
4358        Arc::new(move |left, right| {
4359            Ok(match (left, right) {
4360                (crate::ValueRef::Text(left), crate::ValueRef::Text(right)) => {
4361                    Self::custom_collation_compare(&external, left.as_str(), right.as_str())
4362                }
4363                _ => left.partial_cmp(right).unwrap_or(CmpOrdering::Equal),
4364            })
4365        })
4366    }
4367
4368    pub(crate) fn make_collation_comparator(
4369        &self,
4370        collation: CollationSeq,
4371    ) -> Result<crate::vdbe::sorter::SortComparator> {
4372        let external = self.get_external_collation(collation)?;
4373        Ok(Self::external_collation_comparator(external))
4374    }
4375
4376    pub(crate) fn compare_external_collation(
4377        &self,
4378        collation: CollationSeq,
4379        left: &str,
4380        right: &str,
4381    ) -> Result<CmpOrdering> {
4382        let external = self.get_external_collation(collation)?;
4383        Ok(Self::custom_collation_compare(&external, left, right))
4384    }
4385
4386    pub(crate) fn database_ptr(&self) -> usize {
4387        Arc::as_ptr(&self.db) as usize
4388    }
4389
4390    pub fn set_encryption_key(&self, key: EncryptionKey) -> Result<()> {
4391        tracing::trace!("setting encryption key for connection");
4392        self.ensure_can_change_encryption_settings()?;
4393        *self.encryption_key.write() = Some(key);
4394        self.bump_prepare_context_generation();
4395        self.set_encryption_context()
4396    }
4397
4398    pub fn set_encryption_cipher(&self, cipher_mode: CipherMode) -> Result<()> {
4399        tracing::trace!("setting encryption cipher for connection");
4400        self.ensure_can_change_encryption_settings()?;
4401        self.encryption_cipher_mode.set(cipher_mode);
4402        self.bump_prepare_context_generation();
4403        self.set_encryption_context()
4404    }
4405
4406    pub fn set_reserved_bytes(&self, reserved_bytes: u8) -> Result<()> {
4407        let pager = self.pager.load();
4408        pager.set_reserved_space_bytes(reserved_bytes);
4409        Ok(())
4410    }
4411
4412    /// Get the reserved bytes value from the pager cache.
4413    /// Returns None if not yet set (database not initialized).
4414    pub fn get_reserved_bytes(&self) -> Option<u8> {
4415        let pager = self.pager.load();
4416        pager.get_reserved_space()
4417    }
4418
4419    pub fn get_encryption_cipher_mode(&self) -> Option<CipherMode> {
4420        match self.encryption_cipher_mode.get() {
4421            CipherMode::None => None,
4422            mode => Some(mode),
4423        }
4424    }
4425
4426    fn ensure_can_change_encryption_settings(&self) -> Result<()> {
4427        let pager = self.pager.load();
4428        if pager.is_encryption_ctx_set() {
4429            return Err(LimboError::InvalidArgument(
4430                "cannot reset encryption attributes if already set in the session".to_string(),
4431            ));
4432        }
4433        if self.db.get_mv_store().is_some() {
4434            return Err(LimboError::InvalidArgument(
4435                "cannot enable encryption after MVCC is active; configure encryption before PRAGMA journal_mode='mvcc'"
4436                    .to_string(),
4437            ));
4438        }
4439        Ok(())
4440    }
4441
4442    // if both key and cipher are set, set encryption context on pager
4443    fn set_encryption_context(&self) -> Result<()> {
4444        let key_guard = self.encryption_key.read();
4445        let Some(key) = key_guard.as_ref() else {
4446            return Ok(());
4447        };
4448        let cipher_mode = self.get_encryption_cipher_mode();
4449        let Some(cipher_mode) = cipher_mode else {
4450            return Ok(());
4451        };
4452        tracing::trace!("setting encryption ctx for connection");
4453        let pager = self.pager.load();
4454        pager.set_encryption_context(cipher_mode, key)
4455    }
4456
4457    /// Sets a custom busy handler callback.
4458    pub fn set_busy_handler(&self, handler: Option<BusyHandlerCallback>) {
4459        *self.busy_handler.write() = match handler {
4460            Some(callback) => BusyHandler::Custom { callback },
4461            None => BusyHandler::None,
4462        };
4463        self.bump_prepare_context_generation();
4464    }
4465
4466    /// Sets maximum total accumulated timeout. If the duration is Zero, we unset the busy handler.
4467    pub fn set_busy_timeout(&self, duration: Duration) {
4468        *self.busy_handler.write() = if duration.is_zero() {
4469            BusyHandler::None
4470        } else {
4471            BusyHandler::Timeout(duration)
4472        };
4473        self.bump_prepare_context_generation();
4474    }
4475
4476    /// Get the busy timeout duration.
4477    pub fn get_busy_timeout(&self) -> Duration {
4478        match &*self.busy_handler.read() {
4479            BusyHandler::Timeout(d) => *d,
4480            _ => Duration::ZERO,
4481        }
4482    }
4483
4484    /// Sets the maximum duration a statement is allowed to run.
4485    /// `Duration::ZERO` disables query timeout.
4486    pub fn set_query_timeout(&self, duration: Duration) {
4487        let millis = duration.as_millis().min(u128::from(u64::MAX)) as u64;
4488        self.query_timeout_ms.store(millis, Ordering::SeqCst);
4489    }
4490
4491    /// Get the query timeout duration.
4492    pub fn get_query_timeout(&self) -> Duration {
4493        Duration::from_millis(self.query_timeout_ms.load(Ordering::SeqCst))
4494    }
4495
4496    /// Get a reference to the busy handler.
4497    pub fn get_busy_handler(&self) -> crate::sync::RwLockReadGuard<'_, BusyHandler> {
4498        self.busy_handler.read()
4499    }
4500
4501    /// Sets a progress handler invoked approximately every `ops` VM steps.
4502    /// Passing `ops == 0` or `None` disables the progress handler.
4503    pub fn set_progress_handler(&self, ops: u64, handler: Option<ProgressHandlerCallback>) {
4504        self.progress_handler.set(ops, handler);
4505    }
4506
4507    /// Returns true when the step-based progress handler requests interruption.
4508    pub fn should_interrupt_for_progress(&self, vm_steps: u64) -> bool {
4509        self.progress_handler.should_interrupt(vm_steps)
4510    }
4511
4512    /// Request interruption of currently running root statements on this connection.
4513    /// If no root statement is active, the request is ignored to match SQLite semantics.
4514    pub fn interrupt(&self) {
4515        if self.n_active_root_statements.load(Ordering::SeqCst) > 0 {
4516            self.interrupt_requested.store(true, Ordering::SeqCst);
4517        }
4518    }
4519
4520    /// Returns true if an interrupt is currently pending for this connection.
4521    pub fn is_interrupted(&self) -> bool {
4522        self.interrupt_requested.load(Ordering::SeqCst)
4523    }
4524
4525    /// Clear the connection interrupt once no root statements remain active.
4526    pub(crate) fn clear_interrupt_if_idle(&self) {
4527        if self.n_active_root_statements.load(Ordering::SeqCst) == 0 {
4528            self.interrupt_requested.store(false, Ordering::SeqCst);
4529        }
4530    }
4531
4532    pub(crate) fn set_tx_state(&self, state: TransactionState) {
4533        self.transaction_state.set(state);
4534    }
4535
4536    pub(crate) fn get_tx_state(&self) -> TransactionState {
4537        self.transaction_state.get()
4538    }
4539
4540    /// Returns true if the connection is currently in a write transaction.
4541    /// Used by index methods to determine if it's safe to flush writes.
4542    pub fn is_in_write_tx(&self) -> bool {
4543        matches!(self.get_tx_state(), TransactionState::Write { .. })
4544    }
4545
4546    pub(crate) fn get_mv_tx_id(&self) -> Option<u64> {
4547        self.mv_tx.read().map(|(tx_id, _)| tx_id)
4548    }
4549
4550    pub(crate) fn get_mv_tx(&self) -> Option<(u64, TransactionMode)> {
4551        *self.mv_tx.read()
4552    }
4553
4554    #[inline(always)]
4555    pub(crate) fn set_mv_tx(&self, tx_id_and_mode: Option<(u64, TransactionMode)>) {
4556        tracing::debug!("set_mv_tx: {:?}", tx_id_and_mode);
4557        *self.mv_tx.write() = tx_id_and_mode;
4558    }
4559
4560    /// Get MVCC transaction ID for a specific database.
4561    /// Uses fast path for main DB, O(1) HashMap lookup for attached DBs.
4562    pub(crate) fn get_mv_tx_id_for_db(&self, db: usize) -> Option<u64> {
4563        if db == crate::MAIN_DB_ID {
4564            self.get_mv_tx_id()
4565        } else {
4566            self.attached_mv_txs
4567                .read()
4568                .get(&db)
4569                .map(|(tx_id, _)| *tx_id)
4570        }
4571    }
4572
4573    /// Get MVCC transaction ID and mode for a specific database.
4574    pub(crate) fn get_mv_tx_for_db(&self, db: usize) -> Option<(u64, TransactionMode)> {
4575        if db == crate::MAIN_DB_ID {
4576            self.get_mv_tx()
4577        } else {
4578            self.attached_mv_txs.read().get(&db).copied()
4579        }
4580    }
4581
4582    /// Set MVCC transaction for a specific database.
4583    pub(crate) fn set_mv_tx_for_db(&self, db: usize, val: Option<(u64, TransactionMode)>) {
4584        if db == crate::MAIN_DB_ID {
4585            self.set_mv_tx(val);
4586        } else {
4587            let mut txs = self.attached_mv_txs.write();
4588            match val {
4589                Some(v) => {
4590                    txs.insert(db, v);
4591                }
4592                None => {
4593                    txs.remove(&db);
4594                }
4595            }
4596        }
4597    }
4598
4599    /// Rollback MVCC transactions on all attached databases and clear the
4600    /// attached transaction list.  When `clear_schemas` is true the
4601    /// connection-local schema cache for each attached DB is also removed so
4602    /// that post-rollback queries see the committed schema.
4603    ///
4604    /// This is the single source of truth for attached-MVCC rollback logic —
4605    /// callers in `close()`, `rollback_current_txn()`, and `op_auto_commit`
4606    /// should all delegate here.
4607    pub(crate) fn rollback_attached_mvcc_txs(&self, clear_schemas: bool) {
4608        let txs: HashMap<usize, _> = self.attached_mv_txs.read().clone();
4609        let mut cleared_any_schema = false;
4610        for (&db_id, &(tx_id, _mode)) in &txs {
4611            if let Some(attached_mv_store) = self.mv_store_for_db(db_id) {
4612                let attached_pager = self
4613                    .get_pager_from_database_index(&db_id)
4614                    .expect("attached MVCC transaction should always have a pager");
4615                if attached_mv_store.is_tx_rollbackable(tx_id) {
4616                    attached_mv_store.rollback_tx(tx_id, attached_pager.clone(), self, db_id);
4617                } else {
4618                    self.set_mv_tx_for_db(db_id, None);
4619                }
4620                if clear_schemas {
4621                    self.database_schemas().write().remove(&db_id);
4622                    cleared_any_schema = true;
4623                }
4624                attached_pager.end_read_tx();
4625            }
4626        }
4627        self.attached_mv_txs.write().clear();
4628        if cleared_any_schema {
4629            self.bump_prepare_context_generation();
4630        }
4631    }
4632
4633    /// Rollback WAL-mode transactions on all attached databases and discard
4634    /// their connection-local schema caches.  MVCC-enabled attached databases
4635    /// are skipped — those are handled by `rollback_attached_mvcc_txs`.
4636    pub(crate) fn rollback_attached_wal_txns(&self) {
4637        self.with_all_attached_pagers_with_index(|pagers| {
4638            // Record indices of WAL-mode entries so we can batch the schema
4639            // removal under a single write lock and avoid calling
4640            // `mv_store_for_db` more than once per entry.
4641            let mut wal_indices: SmallVec<[usize; 4]> = SmallVec::new();
4642            for (i, (db_id, _)) in pagers.iter().enumerate() {
4643                if self.mv_store_for_db(*db_id).is_none() {
4644                    wal_indices.push(i);
4645                }
4646            }
4647            if wal_indices.is_empty() {
4648                return;
4649            }
4650            {
4651                let mut schemas = self.database_schemas().write();
4652                for &i in &wal_indices {
4653                    schemas.remove(&pagers[i].0);
4654                }
4655            }
4656            self.bump_prepare_context_generation();
4657            for &i in &wal_indices {
4658                pagers[i].1.rollback_attached();
4659            }
4660        });
4661    }
4662
4663    pub(crate) fn with_named_savepoints<F, T>(&self, f: F) -> T
4664    where
4665        F: FnOnce(&[NamedSavepointFrame]) -> T,
4666    {
4667        let savepoints = self.named_savepoints.read();
4668        f(&savepoints)
4669    }
4670
4671    pub(crate) fn push_named_savepoint(&self, frame: NamedSavepointFrame) {
4672        self.named_savepoints.write().push(frame);
4673    }
4674
4675    /// Snapshot the in-memory schemas (main, temp, attached) for a
4676    /// savepoint frame so ROLLBACK TO can restore them without re-
4677    /// reading sqlite_schema from disk. Disk reparse from inside the
4678    /// vdbe ROLLBACK TO opcode would block on cursor I/O and violate
4679    /// the vdbe async contract.
4680    pub(crate) fn with_savepoint_schema_snapshot<F, T>(&self, f: F) -> T
4681    where
4682        F: FnOnce(Arc<Schema>, Option<Arc<Schema>>, HashMap<usize, Arc<Schema>>) -> T,
4683    {
4684        let main_schema_snapshot = self.schema.read().clone();
4685        let temp_schema_snapshot = self
4686            .temp
4687            .database
4688            .read()
4689            .as_ref()
4690            .map(|temp_db| temp_db.db.schema.lock().clone());
4691        let staged_schema_snapshot = self.database_schemas.read().clone();
4692        f(
4693            main_schema_snapshot,
4694            temp_schema_snapshot,
4695            staged_schema_snapshot,
4696        )
4697    }
4698
4699    pub(crate) fn release_named_savepoint_frame(&self, name: &str) -> SavepointResult {
4700        let mut savepoints = self.named_savepoints.write();
4701        let Some(target_idx) = savepoints
4702            .iter()
4703            .rposition(|savepoint| savepoint.name == name)
4704        else {
4705            return SavepointResult::NotFound;
4706        };
4707        if savepoints[target_idx].starts_transaction && target_idx == 0 {
4708            return SavepointResult::Commit;
4709        }
4710        savepoints.truncate(target_idx);
4711        SavepointResult::Release
4712    }
4713
4714    pub(crate) fn rollback_named_savepoint_frame(&self, name: &str) -> Option<RollbackFrameInfo> {
4715        let mut savepoints = self.named_savepoints.write();
4716        let target_idx = savepoints
4717            .iter()
4718            .rposition(|savepoint| savepoint.name == name)?;
4719        let frame = &savepoints[target_idx];
4720        let info = RollbackFrameInfo {
4721            main_schema_snapshot: frame.main_schema_snapshot.clone(),
4722            temp_schema_snapshot: frame.temp_schema_snapshot.clone(),
4723            staged_schema_snapshot: frame.staged_schema_snapshot.clone(),
4724        };
4725        // ROLLBACK TO keeps the target savepoint itself on the stack;
4726        // only nested savepoints above it are discarded.
4727        savepoints.truncate(target_idx + 1);
4728        Some(info)
4729    }
4730
4731    pub(crate) fn clear_named_savepoints(&self) {
4732        self.named_savepoints.write().clear();
4733    }
4734
4735    /// Roll back the current main-db transaction state and any attached-db
4736    /// transaction state on this connection.
4737    pub(crate) fn rollback_current_txn_state(
4738        &self,
4739        pager: &Arc<Pager>,
4740        clear_attached_schemas: bool,
4741    ) {
4742        if let Some(mv_store) = self.mv_store().as_ref() {
4743            if let Some(tx_id) = self.get_mv_tx_id() {
4744                self.auto_commit.store(true, Ordering::SeqCst);
4745                if mv_store.is_tx_rollbackable(tx_id) {
4746                    mv_store.rollback_tx(tx_id, pager.clone(), self, crate::MAIN_DB_ID);
4747                } else {
4748                    self.set_mv_tx(None);
4749                }
4750            }
4751            pager.end_read_tx();
4752            self.rollback_attached_mvcc_txs(clear_attached_schemas);
4753        } else {
4754            pager.rollback_tx(self);
4755            self.auto_commit.store(true, Ordering::SeqCst);
4756        }
4757        self.rollback_attached_wal_txns();
4758        self.set_tx_state(TransactionState::None);
4759        self.clear_tx_poison();
4760    }
4761
4762    /// Roll back transaction state for helpers that start a manual `BEGIN`
4763    /// outside the normal Transaction opcode path.
4764    ///
4765    /// Unlike `rollback_current_txn_state`, this tolerates the attached-only
4766    /// case where the connection flipped `auto_commit` off but never opened a
4767    /// main-db read transaction.
4768    pub(crate) fn rollback_manual_txn_cleanup(
4769        &self,
4770        pager: &Arc<Pager>,
4771        clear_attached_schemas: bool,
4772    ) {
4773        let main_has_implicit_state = self.get_tx_state() != TransactionState::None
4774            || self.get_mv_tx().is_some()
4775            || pager.holds_read_lock()
4776            || pager.holds_write_lock();
4777
4778        if main_has_implicit_state {
4779            self.rollback_current_txn_state(pager, clear_attached_schemas);
4780        } else {
4781            if self.next_attached_mv_tx().is_some() {
4782                self.rollback_attached_mvcc_txs(clear_attached_schemas);
4783            }
4784            self.rollback_attached_wal_txns();
4785            self.set_tx_state(TransactionState::None);
4786            self.auto_commit.store(true, Ordering::SeqCst);
4787        }
4788
4789        self.rollback_temp_schema();
4790        self.clear_tx_poison();
4791        self.set_cdc_transaction_id(-1);
4792        self.clear_named_savepoints();
4793        self.clear_deferred_foreign_key_violations();
4794    }
4795
4796    /// Iterate over all attached MVCC transactions, calling `f(db_id, tx_id)` for each.
4797    pub(crate) fn for_each_attached_mv_tx(&self, mut f: impl FnMut(usize, u64)) {
4798        let txs = self.attached_mv_txs.read();
4799        for (&db_id, &(tx_id, _)) in txs.iter() {
4800            f(db_id, tx_id);
4801        }
4802    }
4803
4804    /// Get the next attached MVCC transaction.
4805    /// Returns an arbitrary entry from `attached_mv_txs`, or `None` if empty.
4806    pub(crate) fn next_attached_mv_tx(&self) -> Option<(usize, u64, TransactionMode)> {
4807        self.attached_mv_txs
4808            .read()
4809            .iter()
4810            .next()
4811            .map(|(&db_id, &(tx_id, mode))| (db_id, tx_id, mode))
4812    }
4813
4814    /// Get the MvStore for a specific database.
4815    /// Returns None for databases without MVCC or for bootstrap connections.
4816    pub(crate) fn mv_store_for_db(&self, db: usize) -> Option<Arc<MvStore>> {
4817        if self.is_mvcc_bootstrap_connection() {
4818            return None;
4819        }
4820        match db {
4821            crate::MAIN_DB_ID => self.db.get_mv_store().as_ref().cloned(),
4822            crate::TEMP_DB_ID => None,
4823            _ => {
4824                let catalog = self.attached_databases.read();
4825                catalog
4826                    .index_to_data
4827                    .get(&db)
4828                    .and_then(|(db, _)| db.get_mv_store().as_ref().cloned())
4829            }
4830        }
4831    }
4832
4833    pub(crate) fn set_mvcc_checkpoint_threshold(&self, threshold: i64) -> Result<()> {
4834        match self.db.get_mv_store().as_ref() {
4835            Some(mv_store) => {
4836                mv_store.set_checkpoint_threshold(threshold);
4837                self.bump_prepare_context_generation();
4838                Ok(())
4839            }
4840            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4841        }
4842    }
4843
4844    pub(crate) fn mvcc_checkpoint_threshold(&self) -> Result<i64> {
4845        match self.db.get_mv_store().as_ref() {
4846            Some(mv_store) => Ok(mv_store.checkpoint_threshold()),
4847            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4848        }
4849    }
4850
4851    pub(crate) fn set_mvcc_gc_threshold(&self, threshold: i64) -> Result<()> {
4852        match self.db.get_mv_store().as_ref() {
4853            Some(mv_store) => {
4854                mv_store.set_gc_threshold(threshold);
4855                self.bump_prepare_context_generation();
4856                Ok(())
4857            }
4858            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4859        }
4860    }
4861
4862    pub(crate) fn mvcc_gc_threshold(&self) -> Result<i64> {
4863        match self.db.get_mv_store().as_ref() {
4864            Some(mv_store) => Ok(mv_store.gc_threshold()),
4865            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4866        }
4867    }
4868
4869    pub(crate) fn mvcc_tx_should_abort(&self) -> bool {
4870        match (self.db.get_mv_store().clone(), self.get_mv_tx_id()) {
4871            (Some(mv_store), Some(tx_id)) => mv_store.tx_should_abort(tx_id),
4872            _ => false,
4873        }
4874    }
4875}
4876
4877pub type Row = vdbe::Row;
4878
4879pub type StepResult = vdbe::StepResult;
4880
4881#[derive(Default)]
4882pub struct SymbolTable {
4883    pub functions: HashMap<String, Arc<function::ExternalFunc>>,
4884    pub collations: HashMap<u32, Arc<function::ExternalCollation>>,
4885    pub vtabs: HashMap<String, Arc<VirtualTable>>,
4886    pub vtab_modules: HashMap<String, Arc<crate::ext::VTabImpl>>,
4887    pub index_methods: HashMap<String, Arc<dyn IndexMethod>>,
4888}
4889
4890impl std::fmt::Debug for SymbolTable {
4891    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4892        f.debug_struct("SymbolTable")
4893            .field("functions", &self.functions)
4894            .field("collations", &self.collations)
4895            .finish()
4896    }
4897}
4898
4899fn is_shared_library(path: &std::path::Path) -> bool {
4900    path.extension()
4901        .is_some_and(|ext| ext == "so" || ext == "dylib" || ext == "dll")
4902}
4903
4904pub fn resolve_ext_path(extpath: &str) -> Result<std::path::PathBuf> {
4905    let path = std::path::Path::new(extpath);
4906    if !path.exists() {
4907        if is_shared_library(path) {
4908            return Err(LimboError::ExtensionError(format!(
4909                "Extension file not found: {extpath}"
4910            )));
4911        };
4912        let maybe = path.with_extension(std::env::consts::DLL_EXTENSION);
4913        maybe.exists().then_some(maybe).ok_or_else(|| {
4914            LimboError::ExtensionError(format!("Extension file not found: {extpath}"))
4915        })
4916    } else {
4917        Ok(path.to_path_buf())
4918    }
4919}
4920
4921impl SymbolTable {
4922    pub fn new() -> Self {
4923        Self {
4924            functions: HashMap::default(),
4925            collations: HashMap::default(),
4926            vtabs: HashMap::default(),
4927            vtab_modules: HashMap::default(),
4928            index_methods: HashMap::default(),
4929        }
4930    }
4931    pub fn resolve_function(
4932        &self,
4933        name: &str,
4934        arg_count: usize,
4935    ) -> Option<Arc<function::ExternalFunc>> {
4936        self.functions
4937            .get(name)
4938            .cloned()
4939            .or_else(|| {
4940                self.functions
4941                    .get(&crate::util::normalize_ident(name))
4942                    .cloned()
4943            })
4944            .filter(|func| func.func.matches_arg_count(arg_count))
4945    }
4946
4947    pub fn resolve_collation(&self, name: &str) -> Option<CollationSeq> {
4948        let collation = CollationSeq::known_custom(name)?;
4949        self.collations
4950            .contains_key(&collation.id())
4951            .then_some(collation)
4952    }
4953
4954    pub fn extend(&mut self, other: &SymbolTable) {
4955        for (name, func) in &other.functions {
4956            self.functions.insert(name.clone(), func.clone());
4957        }
4958        for (id, collation) in &other.collations {
4959            self.collations.insert(*id, collation.clone());
4960        }
4961        for (name, vtab) in &other.vtabs {
4962            self.vtabs.insert(name.clone(), vtab.clone());
4963        }
4964        for (name, module) in &other.vtab_modules {
4965            self.vtab_modules.insert(name.clone(), module.clone());
4966        }
4967        for (name, module) in &other.index_methods {
4968            self.index_methods.insert(name.clone(), module.clone());
4969        }
4970    }
4971}
4972
4973#[cfg(all(clt_turso_tests, clt_turso_feature = "fs"))]
4974mod tests {
4975    use super::*;
4976    use tempfile::TempDir;
4977
4978    fn open_connection_with_opts(path: &std::path::Path, opts: DatabaseOpts) -> Arc<Connection> {
4979        let io: Arc<dyn IO> = Arc::new(crate::PlatformIO::new().unwrap());
4980        let db = Database::open_file_with_flags(
4981            io,
4982            path.to_str().unwrap(),
4983            OpenFlags::default(),
4984            opts,
4985            None,
4986        )
4987        .unwrap();
4988        db.connect().unwrap()
4989    }
4990
4991    fn open_connection(path: &std::path::Path) -> Arc<Connection> {
4992        open_connection_with_opts(path, DatabaseOpts::new())
4993    }
4994
4995    fn drive_attach(conn: &Arc<Connection>, path: &str, alias: &str) -> Result<()> {
4996        let mut state = AttachDatabaseState::default();
4997        loop {
4998            match conn.attach_database(path, alias, &mut state)? {
4999                IOResult::Done(()) => return Ok(()),
5000                IOResult::IO(io) => io.wait(conn.db.io.as_ref())?,
5001            }
5002        }
5003    }
5004
5005    fn drive_attach_with_config(
5006        conn: &Arc<Connection>,
5007        path: &str,
5008        alias: &str,
5009        reserved_space: Option<u8>,
5010    ) -> Result<()> {
5011        let mut state = AttachDatabaseState::default();
5012        loop {
5013            match conn.attach_database_with_config(path, alias, reserved_space, &mut state)? {
5014                IOResult::Done(()) => return Ok(()),
5015                IOResult::IO(io) => io.wait(conn.db.io.as_ref())?,
5016            }
5017        }
5018    }
5019
5020    fn query_single_i64(conn: &Arc<Connection>, sql: &str) -> i64 {
5021        let mut stmt = conn.prepare(sql).unwrap();
5022        match stmt.step().unwrap() {
5023            StepResult::Row => stmt.row().unwrap().get::<i64>(0).unwrap(),
5024            other => panic!("expected a row, got {other:?}"),
5025        }
5026    }
5027
5028    fn text_value(value: &Value) -> &str {
5029        match value {
5030            Value::Text(text) => text.as_str(),
5031            other => panic!("expected text value, got {other:?}"),
5032        }
5033    }
5034
5035    // given a attached 'alias', return the Database and Pager for that attached database
5036    fn attached_entry(conn: &Connection, alias: &str) -> (Arc<Database>, Arc<Pager>) {
5037        let catalog = conn.attached_databases.read();
5038        let index = *catalog.name_to_index.get(alias).unwrap();
5039        catalog.index_to_data.get(&index).unwrap().clone()
5040    }
5041
5042    #[test]
5043    fn test_named_memory_databases_on_same_io_are_distinct() {
5044        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5045        let draft_db = Database::open_file(io.clone(), ":memory:sync-draft").unwrap();
5046        let synced_db = Database::open_file(io, ":memory:sync-synced").unwrap();
5047        assert!(!Arc::ptr_eq(&draft_db, &synced_db));
5048
5049        let draft = draft_db.connect().unwrap();
5050        let synced = synced_db.connect().unwrap();
5051
5052        for conn in [&draft, &synced] {
5053            assert_eq!(conn.get_database_canonical_path(), "");
5054            assert_eq!(
5055                conn.list_all_databases(),
5056                vec![(MAIN_DB_ID, "main".to_string(), String::new())]
5057            );
5058        }
5059
5060        draft
5061            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(11)")
5062            .unwrap();
5063        synced
5064            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(22)")
5065            .unwrap();
5066
5067        assert_eq!(query_single_i64(&draft, "SELECT x FROM t"), 11);
5068        assert_eq!(query_single_i64(&synced, "SELECT x FROM t"), 22);
5069    }
5070
5071    #[test]
5072    fn test_named_memory_database_reopened_on_same_io_sees_same_rows() {
5073        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5074
5075        let first_db = Database::open_file(io.clone(), ":memory:reopen").unwrap();
5076        let first = first_db.connect().unwrap();
5077        first
5078            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(99)")
5079            .unwrap();
5080
5081        let second_db = Database::open_file(io, ":memory:reopen").unwrap();
5082        let second = second_db.connect().unwrap();
5083        assert_eq!(query_single_i64(&second, "SELECT x FROM t"), 99);
5084    }
5085
5086    #[test]
5087    fn test_attach_named_memory_database_reports_empty_path() {
5088        let temp_dir = TempDir::new().unwrap();
5089        let main_path = temp_dir.path().join("main.db");
5090        let conn = open_connection_with_opts(&main_path, DatabaseOpts::new().with_attach(true));
5091
5092        conn.execute("ATTACH ':memory:aux' AS aux").unwrap();
5093        conn.execute("CREATE TABLE aux.t(x INTEGER); INSERT INTO aux.t VALUES(5)")
5094            .unwrap();
5095
5096        assert_eq!(query_single_i64(&conn, "SELECT x FROM aux.t"), 5);
5097        let database_list = conn.pragma_query("database_list").unwrap();
5098        let aux = database_list
5099            .iter()
5100            .find(|row| text_value(&row[1]) == "aux")
5101            .expect("attached aux database must be listed");
5102        assert_eq!(text_value(&aux[2]), "");
5103    }
5104
5105    #[test]
5106    fn test_named_memory_parent_can_attach_real_file_database() {
5107        let temp_dir = TempDir::new().unwrap();
5108        let aux_path = temp_dir.path().join("aux.db");
5109        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5110        let db = Database::open_file_with_flags(
5111            io,
5112            ":memory:named-main",
5113            OpenFlags::default(),
5114            DatabaseOpts::new().with_attach(true),
5115            None,
5116        )
5117        .unwrap();
5118        let conn = db.connect().unwrap();
5119
5120        conn.execute(format!("ATTACH '{}' AS aux", aux_path.to_str().unwrap()))
5121            .unwrap();
5122        conn.execute("CREATE TABLE aux.t(x INTEGER); INSERT INTO aux.t VALUES(7)")
5123            .unwrap();
5124        conn.execute("DETACH aux").unwrap();
5125
5126        let reopened = open_connection(&aux_path);
5127        assert_eq!(query_single_i64(&reopened, "SELECT x FROM t"), 7);
5128    }
5129
5130    #[test]
5131    fn test_attach_database_with_config_overrides_reserved_space_before_initialization() {
5132        let temp_dir = TempDir::new().unwrap();
5133        let main_path = temp_dir.path().join("main.db");
5134        let aux_path = temp_dir.path().join("aux.db");
5135        let conn = open_connection(&main_path);
5136
5137        drive_attach_with_config(&conn, aux_path.to_str().unwrap(), "aux", Some(48)).unwrap();
5138
5139        let (attached_db, pager) = attached_entry(&conn, "aux");
5140        assert!(!attached_db.initialized());
5141        assert!(!pager.db_initialized());
5142        assert_eq!(pager.get_reserved_space(), Some(48));
5143    }
5144
5145    #[cfg(clt_turso_feature = "checksum")]
5146    #[test]
5147    fn test_attach_database_with_config_rejects_reserved_space_below_minimum() {
5148        let temp_dir = TempDir::new().unwrap();
5149        let main_path = temp_dir.path().join("main.db");
5150        let aux_path = temp_dir.path().join("aux.db");
5151        let conn = open_connection(&main_path);
5152
5153        let err = drive_attach_with_config(&conn, aux_path.to_str().unwrap(), "aux", Some(0))
5154            .unwrap_err()
5155            .to_string();
5156        assert_eq!(
5157            err,
5158            "Invalid argument supplied: cannot attach database 'aux': reserved space 0 is smaller than attached database minimum 8"
5159        );
5160    }
5161
5162    #[test]
5163    fn test_fresh_mvcc_attach_installs_wal_before_bootstrap() {
5164        // this is a test to check that mvcc db on attach with a fresh db, makes the
5165        // attached db also mvcc
5166        let temp_dir = TempDir::new().unwrap();
5167        let main_path = temp_dir.path().join("main.db");
5168        let aux_path = temp_dir.path().join("aux.db");
5169        let conn = open_connection(&main_path);
5170
5171        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5172        drive_attach(&conn, aux_path.to_str().unwrap(), "aux").unwrap();
5173
5174        let (attached_db, pager) = attached_entry(&conn, "aux");
5175        assert!(attached_db.get_mv_store().as_ref().is_some());
5176        assert!(pager.has_wal());
5177
5178        conn.execute("CREATE TABLE aux.t(x INTEGER)").unwrap();
5179        conn.execute("INSERT INTO aux.t VALUES(1)").unwrap();
5180        conn.execute("PRAGMA aux.wal_checkpoint(TRUNCATE)").unwrap();
5181    }
5182
5183    #[test]
5184    fn test_fresh_mvcc_attach_reuses_database_shared_wal() {
5185        let temp_dir = TempDir::new().unwrap();
5186        let main_path = temp_dir.path().join("main.db");
5187        let aux_path = temp_dir.path().join("aux.db");
5188        let conn = open_connection(&main_path);
5189
5190        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5191        drive_attach(&conn, aux_path.to_str().unwrap(), "aux").unwrap();
5192        conn.execute("CREATE TABLE aux.t(x INTEGER)").unwrap();
5193        conn.execute("INSERT INTO aux.t VALUES(1)").unwrap();
5194
5195        let (attached_db, pager) = attached_entry(&conn, "aux");
5196        let pager_shared_ptr = pager
5197            .wal_shared_ptr()
5198            .expect("fresh MVCC attach must expose WAL shared state in tests");
5199        let db_shared_ptr = Arc::as_ptr(&attached_db.shared_wal) as usize;
5200
5201        assert_eq!(pager_shared_ptr, db_shared_ptr);
5202    }
5203
5204    #[test]
5205    fn test_temp_tables_are_connection_local_and_shadow_main() {
5206        let temp_dir = TempDir::new().unwrap();
5207        let db_path = temp_dir.path().join("main.db");
5208        let conn1 = open_connection(&db_path);
5209
5210        conn1.execute("CREATE TABLE t(x INTEGER)").unwrap();
5211        conn1.execute("INSERT INTO main.t VALUES(1)").unwrap();
5212        let conn2 = open_connection(&db_path);
5213        conn1.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5214        conn1.execute("INSERT INTO temp.t VALUES(2)").unwrap();
5215
5216        assert_eq!(query_single_i64(&conn1, "SELECT x FROM t"), 2);
5217        assert_eq!(query_single_i64(&conn1, "SELECT x FROM main.t"), 1);
5218        assert_eq!(query_single_i64(&conn2, "SELECT x FROM t"), 1);
5219
5220        let err = conn2
5221            .prepare("SELECT x FROM temp.t")
5222            .unwrap_err()
5223            .to_string();
5224        assert!(
5225            err.contains("no such table"),
5226            "expected no such table error, got: {err}"
5227        );
5228    }
5229
5230    #[test]
5231    fn test_reprepare_after_temp_store_reset_does_not_panic() {
5232        let temp_dir = TempDir::new().unwrap();
5233        let db_path = temp_dir.path().join("main.db");
5234        let conn = open_connection(&db_path);
5235
5236        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5237        let mut stmt = conn.prepare("SELECT x FROM t").unwrap();
5238
5239        conn.execute("PRAGMA temp_store = MEMORY").unwrap();
5240
5241        let err = stmt.step().unwrap_err().to_string();
5242        assert!(
5243            err.contains("no such table"),
5244            "expected no such table after temp reset, got: {err}"
5245        );
5246    }
5247
5248    #[test]
5249    fn test_temp_trigger_abort_rolls_back_temp_writes_without_panicking() {
5250        let temp_dir = TempDir::new().unwrap();
5251        let db_path = temp_dir.path().join("main.db");
5252        let conn = open_connection(&db_path);
5253
5254        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5255        conn.execute("CREATE TEMP TABLE u(y INTEGER)").unwrap();
5256        conn.execute(
5257            "CREATE TRIGGER tr BEFORE INSERT ON temp.t BEGIN \
5258             INSERT INTO u VALUES (NEW.x); \
5259             SELECT RAISE(ABORT, 'boom'); \
5260             END;",
5261        )
5262        .unwrap();
5263
5264        let err = conn.execute("INSERT INTO temp.t VALUES(1)").unwrap_err();
5265        assert!(
5266            err.to_string().contains("boom"),
5267            "expected trigger abort error, got: {err}"
5268        );
5269        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.u"), 0);
5270        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.t"), 0);
5271    }
5272
5273    #[test]
5274    fn test_temp_trigger_abort_rolls_back_main_and_temp_writes() {
5275        let temp_dir = TempDir::new().unwrap();
5276        let db_path = temp_dir.path().join("main.db");
5277        let conn = open_connection(&db_path);
5278
5279        conn.execute("CREATE TABLE m(x INTEGER)").unwrap();
5280        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5281        conn.execute("CREATE TEMP TABLE u(y INTEGER)").unwrap();
5282        conn.execute(
5283            "CREATE TRIGGER tr BEFORE INSERT ON temp.t BEGIN \
5284             INSERT INTO m VALUES (NEW.x); \
5285             INSERT INTO u VALUES (NEW.x); \
5286             SELECT RAISE(ABORT, 'boom'); \
5287             END;",
5288        )
5289        .unwrap();
5290
5291        let err = conn.execute("INSERT INTO temp.t VALUES(1)").unwrap_err();
5292        assert!(
5293            err.to_string().contains("boom"),
5294            "expected trigger abort error, got: {err}"
5295        );
5296        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM main.m"), 0);
5297        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.u"), 0);
5298        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.t"), 0);
5299    }
5300
5301    #[test]
5302    fn test_distinct_triggers_with_same_name_in_different_schemas_can_fire_nested() {
5303        let temp_dir = TempDir::new().unwrap();
5304        let db_path = temp_dir.path().join("main.db");
5305        let conn = open_connection(&db_path);
5306
5307        conn.execute("CREATE TABLE src(x INTEGER)").unwrap();
5308        conn.execute("CREATE TABLE dst(y INTEGER)").unwrap();
5309        conn.execute("CREATE TABLE audit(z INTEGER)").unwrap();
5310        conn.execute(
5311            "CREATE TRIGGER shared_name AFTER INSERT ON dst BEGIN \
5312             INSERT INTO audit VALUES (NEW.y); \
5313             END;",
5314        )
5315        .unwrap();
5316        conn.execute(
5317            "CREATE TEMP TRIGGER shared_name AFTER INSERT ON main.src BEGIN \
5318             INSERT INTO dst VALUES (NEW.x); \
5319             END;",
5320        )
5321        .unwrap();
5322
5323        conn.execute("INSERT INTO src VALUES(7)").unwrap();
5324
5325        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM main.dst"), 1);
5326        assert_eq!(query_single_i64(&conn, "SELECT SUM(z) FROM main.audit"), 7);
5327    }
5328
5329    /// A committed `setval(X, false)` stores an unconsumed sequence value.
5330    /// After sequence initialization reloads persisted state, the in-memory
5331    /// sequence must still represent that value as unconsumed, so the next
5332    /// `nextval()` returns `X` rather than advancing past it.
5333    /// Disk-only sequence design: setval(value, is_called=false) must be
5334    /// observable as the next nextval() result. Previously this exercised
5335    /// the in-memory-atomic reseeding path; that path no longer exists,
5336    /// but the user-visible contract still holds because every nextval
5337    /// reads the backing-table watermark and applies is_called semantics
5338    /// in op_sequence_compute_next.
5339    #[test]
5340    fn test_setval_uncalled_emits_stored_value_as_next() -> Result<()> {
5341        let temp_dir = TempDir::new().unwrap();
5342        let path = temp_dir.path().join("seq_init.db");
5343        let conn = open_connection_with_opts(&path, DatabaseOpts::new());
5344
5345        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5346        conn.execute("CREATE SEQUENCE s START 1 INCREMENT 3")?;
5347        conn.execute("SELECT setval('s', 13, 0)")?;
5348
5349        let next_val = query_single_i64(&conn, "SELECT nextval('s')");
5350        assert_eq!(
5351            next_val, 13,
5352            "setval(13, false) committed: next nextval must return 13"
5353        );
5354        Ok(())
5355    }
5356}