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(clt_turso_feature = "simulator", target_pointer_width = "64", host_shared_wal))]
2480    pub fn install_unpublished_backfill_proof_for_testing(
2481        &self,
2482        upper_bound_inclusive: u64,
2483    ) -> Result<()> {
2484        let pager = self.pager.load();
2485        let proof_nbackfills =
2486            pager.run_checkpoint_until_post_sync_gap_for_testing(CheckpointMode::Passive {
2487                upper_bound_inclusive: Some(upper_bound_inclusive),
2488            })?;
2489        let authority = self.db.shared_wal_coordination()?.ok_or_else(|| {
2490            LimboError::InternalError("shared WAL authority is unavailable".into())
2491        })?;
2492        let snapshot_before_publish = authority.snapshot();
2493        if snapshot_before_publish.nbackfills != 0 {
2494            return Err(LimboError::InternalError(
2495                "unpublished-proof setup requires nbackfills to remain unpublished".into(),
2496            ));
2497        }
2498
2499        let (db_size_pages, db_header_crc32c) = db_identity_for_testing(Path::new(&self.db.path))?;
2500        authority.install_backfill_proof(
2501            crate::storage::shared_wal_coordination::SharedWalCoordinationHeader {
2502                nbackfills: proof_nbackfills,
2503                ..snapshot_before_publish
2504            },
2505            db_size_pages,
2506            db_header_crc32c,
2507        );
2508        Ok(())
2509    }
2510
2511    pub fn last_insert_rowid(&self) -> i64 {
2512        self.last_insert_rowid.load(Ordering::SeqCst)
2513    }
2514
2515    pub(crate) fn update_last_rowid(&self, rowid: i64) {
2516        self.last_insert_rowid.store(rowid, Ordering::SeqCst);
2517    }
2518
2519    pub(crate) fn add_total_changes(&self, num_changes: i64) {
2520        self.total_changes.fetch_add(num_changes, Ordering::SeqCst);
2521    }
2522
2523    pub fn set_changes(&self, num_changes: i64) {
2524        self.changes.store(num_changes, Ordering::SeqCst);
2525    }
2526
2527    pub fn changes(&self) -> i64 {
2528        self.changes.load(Ordering::SeqCst)
2529    }
2530
2531    pub fn total_changes(&self) -> i64 {
2532        self.total_changes.load(Ordering::SeqCst)
2533    }
2534
2535    pub fn get_cache_size(&self) -> i32 {
2536        self.cache_size.load(Ordering::SeqCst)
2537    }
2538    pub fn set_cache_size(&self, size: i32) {
2539        self.cache_size.store(size, Ordering::SeqCst);
2540        self.bump_prepare_context_generation();
2541    }
2542
2543    pub fn get_capture_data_changes_info(
2544        &self,
2545    ) -> crate::sync::RwLockReadGuard<'_, Option<CaptureDataChangesInfo>> {
2546        self.capture_data_changes.read()
2547    }
2548    pub fn set_capture_data_changes_info(&self, opts: Option<CaptureDataChangesInfo>) {
2549        *self.capture_data_changes.write() = opts;
2550        self.bump_prepare_context_generation();
2551    }
2552    pub fn get_cdc_transaction_id(&self) -> i64 {
2553        self.cdc_transaction_id.load(Ordering::SeqCst)
2554    }
2555    pub fn set_cdc_transaction_id(&self, id: i64) {
2556        self.cdc_transaction_id.store(id, Ordering::SeqCst);
2557    }
2558    pub fn get_page_size(&self) -> PageSize {
2559        let value = self.page_size.load(Ordering::SeqCst);
2560        PageSize::new_from_header_u16(value).unwrap_or_default()
2561    }
2562
2563    pub fn is_closed(&self) -> bool {
2564        self.closed.load(Ordering::SeqCst)
2565    }
2566
2567    pub fn is_query_only(&self) -> bool {
2568        self.query_only.load(Ordering::SeqCst)
2569    }
2570
2571    pub fn get_database_canonical_path(&self) -> String {
2572        self.db.get_database_canonical_path()
2573    }
2574
2575    /// Check if a specific attached database is read only or not, by its index
2576    pub fn is_readonly(&self, index: usize) -> bool {
2577        match index {
2578            crate::MAIN_DB_ID => self.db.is_readonly(),
2579            crate::TEMP_DB_ID => self
2580                .temp
2581                .database
2582                .read()
2583                .as_ref()
2584                .is_some_and(|temp_db| temp_db.db.is_readonly()),
2585            _ => {
2586                let db = self.attached_databases.read().get_database_by_index(index);
2587                db.expect("Should never have called this without being sure the database exists")
2588                    .is_readonly()
2589            }
2590        }
2591    }
2592
2593    /// Reset the page size for the current connection.
2594    ///
2595    /// Specifying a new page size does not change the page size immediately.
2596    /// Instead, the new page size is remembered and is used to set the page size when the database
2597    /// is first created, if it does not already exist when the page_size pragma is issued,
2598    /// or at the next VACUUM command that is run on the same database connection while not in WAL mode.
2599    pub fn reset_page_size(&self, size: u32) -> Result<()> {
2600        if self.db.initialized() {
2601            return Ok(());
2602        }
2603        let Some(size) = PageSize::new(size) else {
2604            return Ok(());
2605        };
2606
2607        self.page_size.store(size.get_raw(), Ordering::SeqCst);
2608        self.pager.load().set_initial_page_size(size)?;
2609        // MvStore caches a copy of the database header in `global_header`, captured from the
2610        // pager during bootstrap (before any PRAGMA page_size can run). Propagate the new
2611        // page size so subsequent transactions and any header lookups see the same value the
2612        // pager will write to disk; otherwise paths like op_open_ephemeral allocate buffers
2613        // sized to the connection's page_size but compute usable_space from the stale 4 KiB
2614        // global header, tripping the btree_init_page assertion.
2615        if let Some(mv_store) = self.db.get_mv_store().as_ref() {
2616            mv_store.set_global_page_size(size);
2617        }
2618        self.bump_prepare_context_generation();
2619
2620        Ok(())
2621    }
2622
2623    #[cfg(clt_turso_feature = "fs")]
2624    pub fn open_new(&self, path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
2625        Database::open_with_vfs(&self.db, path, vfs)
2626    }
2627
2628    pub fn list_vfs(&self) -> Vec<String> {
2629        #[allow(unused_mut)]
2630        let mut all_vfs = vec![String::from("memory")];
2631        #[cfg(clt_turso_feature = "fs")]
2632        {
2633            #[cfg(target_family = "unix")]
2634            {
2635                all_vfs.push("syscall".to_string());
2636            }
2637            #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring"))]
2638            {
2639                all_vfs.push("io_uring".to_string());
2640            }
2641            #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
2642            {
2643                all_vfs.push("experimental_win_iocp".to_string());
2644            }
2645            all_vfs.extend(crate::ext::list_vfs_modules());
2646        }
2647        all_vfs
2648    }
2649
2650    pub fn get_auto_commit(&self) -> bool {
2651        self.auto_commit.load(Ordering::SeqCst)
2652    }
2653
2654    /// Mark the active explicit transaction poisoned so COMMIT rolls it back.
2655    ///
2656    /// This is used when a write statement under BEGIN is abandoned before it
2657    /// reaches Halt/Done and that statement did not open a statement savepoint.
2658    pub(crate) fn mark_tx_poisoned(&self) {
2659        self.poisoned_tx.store(true, Ordering::SeqCst);
2660    }
2661
2662    /// Return whether the active explicit transaction must roll back at COMMIT.
2663    pub(crate) fn tx_is_poisoned(&self) -> bool {
2664        self.poisoned_tx.load(Ordering::SeqCst)
2665    }
2666
2667    /// Clear the poison marker after BEGIN, COMMIT, or ROLLBACK.
2668    pub(crate) fn clear_tx_poison(&self) {
2669        self.poisoned_tx.store(false, Ordering::SeqCst);
2670    }
2671
2672    pub fn set_load_extension_enabled(&self, enabled: bool) {
2673        self.enable_load_extension.store(enabled, Ordering::Release);
2674    }
2675
2676    pub(crate) fn can_load_extensions(&self) -> bool {
2677        self.enable_load_extension.load(Ordering::Acquire)
2678    }
2679
2680    pub fn reparse_schema_after_extension_load(self: &Arc<Connection>) -> Result<()> {
2681        if self.is_closed() {
2682            return Err(LimboError::InternalError("Connection closed".to_string()));
2683        }
2684        // Collect row data from the Statement first, then drop the Statement
2685        // before taking the schema write lock. This prevents a deadlock in MVCC
2686        // mode where Statement::drop -> abort -> rollback_tx -> schema.read()
2687        // would deadlock against the schema write lock.
2688        let mut rows_data: Vec<(String, String, String, i64, Option<String>)> = Vec::new();
2689        {
2690            let mut rows = self
2691                .query("SELECT * FROM sqlite_schema")?
2692                .expect("query must be parsed to statement");
2693            rows.run_with_row_callback(|row| {
2694                let ty = row.get::<&str>(0)?.to_string();
2695                let name = row.get::<&str>(1)?.to_string();
2696                let table_name = row.get::<&str>(2)?.to_string();
2697                let root_page = row.get::<i64>(3)?;
2698                let sql = row.get::<&str>(4).ok().map(|s| s.to_string());
2699                rows_data.push((ty, name, table_name, root_page, sql));
2700                Ok(())
2701            })?;
2702        } // Statement dropped here, before schema write lock
2703
2704        let syms = self.syms.read();
2705        self.with_schema_mut(|schema| -> Result<()> {
2706            // Incremental re-parse after extension loading. The schema already has
2707            // tables/indices/views from initial parse. We only need to pick up
2708            // entries that previously failed (e.g. virtual tables whose module
2709            // wasn't loaded yet). "Already exists" errors are expected and skipped.
2710            let mut from_sql_indexes = crate::alloc::vec![];
2711            let mut automatic_indices = HashMap::default();
2712            let mut dbsp_state_roots = HashMap::default();
2713            let mut dbsp_state_index_roots = HashMap::default();
2714            let mut materialized_view_info = HashMap::default();
2715
2716            let attached_resolver = |name: &str| -> Option<usize> {
2717                self.attached_databases
2718                    .read()
2719                    .get_database_by_name(&crate::util::normalize_ident(name))
2720                    .map(|(idx, _)| idx)
2721            };
2722            for (ty, name, table_name, root_page, sql) in &rows_data {
2723                match schema.handle_schema_row(
2724                    ty,
2725                    name,
2726                    table_name,
2727                    *root_page,
2728                    sql.as_deref(),
2729                    &syms,
2730                    &mut from_sql_indexes,
2731                    &mut automatic_indices,
2732                    &mut dbsp_state_roots,
2733                    &mut dbsp_state_index_roots,
2734                    &mut materialized_view_info,
2735                    &attached_resolver,
2736                ) {
2737                    Ok(()) => {}
2738                    Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {}
2739                    Err(LimboError::ExtensionError(msg)) => {
2740                        eprintln!("Warning: {msg}");
2741                    }
2742                    Err(e) => return Err(e),
2743                }
2744            }
2745
2746            match schema.populate_indices(&syms, from_sql_indexes, automatic_indices, false) {
2747                Ok(()) => {}
2748                Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {}
2749                Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"),
2750                Err(e) => return Err(e),
2751            }
2752            match schema.populate_materialized_views(
2753                materialized_view_info,
2754                dbsp_state_roots,
2755                dbsp_state_index_roots,
2756            ) {
2757                Ok(()) => {}
2758                Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"),
2759                Err(e) => return Err(e),
2760            }
2761            Ok(())
2762        })?
2763    }
2764
2765    // Clearly there is something to improve here, Vec<Vec<Value>> isn't a couple of tea
2766    /// Query the current rows/values of `pragma_name`.
2767    pub fn pragma_query(self: &Arc<Connection>, pragma_name: &str) -> Result<Vec<Vec<Value>>> {
2768        if self.is_closed() {
2769            return Err(LimboError::InternalError("Connection closed".to_string()));
2770        }
2771        let pragma = format!("PRAGMA {pragma_name}");
2772        let mut stmt = self.prepare(pragma)?;
2773        stmt.run_collect_rows()
2774    }
2775
2776    /// Set a new value to `pragma_name`.
2777    ///
2778    /// Some pragmas will return the updated value which cannot be retrieved
2779    /// with this method.
2780    pub fn pragma_update<V: Display>(
2781        self: &Arc<Connection>,
2782        pragma_name: &str,
2783        pragma_value: V,
2784    ) -> Result<Vec<Vec<Value>>> {
2785        if self.is_closed() {
2786            return Err(LimboError::InternalError("Connection closed".to_string()));
2787        }
2788        let pragma = format!("PRAGMA {pragma_name} = {pragma_value}");
2789        let mut stmt = self.prepare(pragma)?;
2790        stmt.run_collect_rows()
2791    }
2792
2793    pub fn experimental_views_enabled(&self) -> bool {
2794        self.db.experimental_views_enabled()
2795    }
2796
2797    pub fn experimental_index_method_enabled(&self) -> bool {
2798        self.db.experimental_index_method_enabled()
2799    }
2800
2801    pub fn experimental_custom_types_enabled(&self) -> bool {
2802        self.db.experimental_custom_types_enabled()
2803    }
2804
2805    pub fn experimental_attach_enabled(&self) -> bool {
2806        self.db.experimental_attach_enabled()
2807    }
2808
2809    pub fn experimental_vacuum_enabled(&self) -> bool {
2810        self.db.experimental_vacuum_enabled()
2811    }
2812
2813    pub fn experimental_mvcc_passive_checkpoint_enabled(&self) -> bool {
2814        self.db.experimental_mvcc_passive_checkpoint_enabled()
2815    }
2816
2817    pub fn experimental_multiprocess_wal_enabled(&self) -> bool {
2818        self.db.experimental_multiprocess_wal_enabled()
2819    }
2820
2821    pub fn experimental_generated_columns_enabled(&self) -> bool {
2822        self.db.experimental_generated_columns_enabled()
2823    }
2824
2825    pub fn experimental_without_rowid_enabled(&self) -> bool {
2826        self.db.experimental_without_rowid_enabled()
2827    }
2828
2829    pub fn mvcc_enabled(&self) -> bool {
2830        self.db.mvcc_enabled()
2831    }
2832
2833    pub fn mv_store(&self) -> impl Deref<Target = Option<Arc<MvStore>>> {
2834        struct TransparentWrapper<T>(T);
2835
2836        impl<T> Deref for TransparentWrapper<T> {
2837            type Target = T;
2838
2839            fn deref(&self) -> &Self::Target {
2840                &self.0
2841            }
2842        }
2843
2844        // Never use MV store for bootstrapping - we read state directly from sqlite_schema in the DB file.
2845        if !self.is_mvcc_bootstrap_connection() {
2846            either::Left(self.db.get_mv_store())
2847        } else {
2848            either::Right(TransparentWrapper(None))
2849        }
2850    }
2851
2852    #[cfg(any(clt_turso_tests, injected_yields))]
2853    pub fn set_yield_injector(&self, injector: Option<Arc<dyn YieldInjector>>) {
2854        let mut slot = self.yield_injector.write();
2855        match injector {
2856            Some(injector) => {
2857                turso_assert!(
2858                    slot.is_none(),
2859                    "yield injector should be empty before installing a new one"
2860                );
2861                *slot = Some(injector);
2862            }
2863            None => {
2864                turso_assert!(
2865                    slot.is_some(),
2866                    "yield injector should be installed before it is cleared"
2867                );
2868                *slot = None;
2869            }
2870        }
2871    }
2872
2873    #[cfg(any(clt_turso_tests, injected_yields))]
2874    pub(crate) fn yield_injector(&self) -> Option<Arc<dyn YieldInjector>> {
2875        self.yield_injector.read().clone()
2876    }
2877
2878    #[cfg(any(clt_turso_tests, injected_yields))]
2879    pub fn set_failure_injector(&self, injector: Option<Arc<dyn FailureInjector>>) {
2880        let mut slot = self.failure_injector.write();
2881        match injector {
2882            Some(injector) => {
2883                turso_assert!(
2884                    slot.is_none(),
2885                    "failure injector should be empty before installing a new one"
2886                );
2887                *slot = Some(injector);
2888            }
2889            None => {
2890                turso_assert!(
2891                    slot.is_some(),
2892                    "failure injector should be installed before it is cleared"
2893                );
2894                *slot = None;
2895            }
2896        }
2897    }
2898
2899    #[cfg(any(clt_turso_tests, injected_yields))]
2900    pub(crate) fn failure_injector(&self) -> Option<Arc<dyn FailureInjector>> {
2901        self.failure_injector.read().clone()
2902    }
2903
2904    #[cfg(any(clt_turso_tests, injected_yields))]
2905    #[inline(always)]
2906    pub(crate) fn next_yield_instance_id(&self) -> u64 {
2907        self.yield_instance_id_counter
2908            .fetch_add(1, Ordering::Relaxed)
2909    }
2910
2911    /// Query the current value(s) of `pragma_name` associated to
2912    /// `pragma_value`.
2913    ///
2914    /// This method can be used with query-only pragmas which need an argument
2915    /// (e.g. `table_info('one_tbl')`) or pragmas which returns value(s)
2916    /// (e.g. `integrity_check`).
2917    pub fn pragma<V: Display>(
2918        self: &Arc<Connection>,
2919        pragma_name: &str,
2920        pragma_value: V,
2921    ) -> Result<Vec<Vec<Value>>> {
2922        if self.is_closed() {
2923            return Err(LimboError::InternalError("Connection closed".to_string()));
2924        }
2925        let pragma = format!("PRAGMA {pragma_name}({pragma_value})");
2926        let mut stmt = self.prepare(pragma)?;
2927        let mut results = Vec::new();
2928        loop {
2929            match stmt.step()? {
2930                vdbe::StepResult::Row => {
2931                    let row: Vec<Value> = stmt.row().unwrap().get_values().cloned().collect();
2932                    results.push(row);
2933                }
2934                vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => {
2935                    return Err(LimboError::Busy);
2936                }
2937                _ => break,
2938            }
2939        }
2940
2941        Ok(results)
2942    }
2943
2944    #[inline]
2945    pub fn with_schema_mut<T>(&self, f: impl FnOnce(&mut Schema) -> T) -> Result<T> {
2946        let mut schema_ref = self.schema.write();
2947        let schema = Schema::try_make_mut(&mut schema_ref)?;
2948        Ok(f(schema))
2949    }
2950
2951    /// Mutate the schema for a specific database (main or attached).
2952    pub(crate) fn with_database_schema_mut<T>(
2953        &self,
2954        database_id: usize,
2955        f: impl FnOnce(&mut Schema) -> T,
2956    ) -> Result<T> {
2957        match database_id {
2958            crate::MAIN_DB_ID => self.with_schema_mut(f),
2959            crate::TEMP_DB_ID => {
2960                // The temp database is connection-local, no other connection can
2961                // reference its schema, so we can mutate it directly without cloning
2962                // into `database_schemas`.
2963                let temp_db_guard = self.temp.database.read();
2964                let temp_db = temp_db_guard
2965                    .as_ref()
2966                    .expect("temp database should be initialized before schema mutation");
2967                let mut schema_guard = temp_db.db.schema.lock();
2968                let schema = Schema::try_make_mut(&mut schema_guard)?;
2969                let result = f(schema);
2970                self.bump_prepare_context_generation();
2971                Ok(result)
2972            }
2973            _ => {
2974                // For attached databases, update a connection-local copy of the schema.
2975                // We don't update the shared db.schema until after the WAL commit, so
2976                // other connections won't see uncommitted schema changes (which would
2977                // cause SchemaUpdated mismatches).
2978                let mut schemas = self.database_schemas.write();
2979                let schema_arc = schemas.entry(database_id).or_insert_with(|| {
2980                    let attached_dbs = self.attached_databases.read();
2981                    let (db, _pager) = attached_dbs
2982                        .index_to_data
2983                        .get(&database_id)
2984                        .expect("Database ID should be valid");
2985                    let schema = db.schema.lock().clone();
2986                    schema
2987                });
2988                let schema = Schema::try_make_mut(schema_arc)?;
2989                let result = f(schema);
2990                self.bump_prepare_context_generation();
2991                Ok(result)
2992            }
2993        }
2994    }
2995
2996    pub fn is_db_initialized(&self) -> bool {
2997        self.db.initialized()
2998    }
2999
3000    pub(crate) fn get_pager_from_database_index(&self, index: &usize) -> Result<Arc<Pager>> {
3001        match *index {
3002            crate::MAIN_DB_ID => Ok(self.pager.load().clone()),
3003            crate::TEMP_DB_ID => {
3004                // Lazily initialize the temp database if it hasn't been created yet.
3005                if self.temp.database.read().is_none() {
3006                    self.ensure_temp_database()?;
3007                }
3008                Ok(self
3009                    .temp
3010                    .database
3011                    .read()
3012                    .as_ref()
3013                    .map(|temp_db| temp_db.pager.clone())
3014                    .expect("temp database should be initialized after ensure_temp_database"))
3015            }
3016            _ => Ok(self.attached_databases.read().get_pager_by_index(index)),
3017        }
3018    }
3019
3020    /// Get the database name for a given database index.
3021    /// Returns "main" for index 0, "temp" for index 1, and the alias for attached databases.
3022    pub(crate) fn get_database_name_by_index(&self, index: usize) -> Option<String> {
3023        match index {
3024            MAIN_DB_ID => Some("main".to_string()),
3025            TEMP_DB_ID => Some("temp".to_string()),
3026            _ => self.attached_databases.read().get_name_by_index(index),
3027        }
3028    }
3029
3030    /// Get the database id for a schema name ("main", "temp", or an attached db alias).
3031    pub(crate) fn get_database_id_by_name(&self, name: &str) -> Result<usize> {
3032        let normalized: String = crate::util::normalize_ident(name);
3033        match normalized.as_str() {
3034            "main" => Ok(MAIN_DB_ID),
3035            "temp" => Ok(TEMP_DB_ID),
3036            _ => self
3037                .attached_databases
3038                .read()
3039                .get_database_by_name(&normalized)
3040                .map(|(idx, _)| idx)
3041                .ok_or_else(|| LimboError::InvalidArgument(format!("no such database: {name}"))),
3042        }
3043    }
3044
3045    /// Get the Database object for a given database id.
3046    pub(crate) fn get_source_database(&self, database_id: usize) -> Arc<Database> {
3047        match database_id {
3048            MAIN_DB_ID => self.db.clone(),
3049            TEMP_DB_ID => self
3050                .temp
3051                .database
3052                .read()
3053                .as_ref()
3054                .map(|temp_db| temp_db.db.clone())
3055                .unwrap_or_else(|| self.db.clone()),
3056            _ => self
3057                .attached_databases
3058                .read()
3059                .get_database_by_index(database_id)
3060                .expect("database index should be valid"),
3061        }
3062    }
3063
3064    fn is_attached(&self, alias: &str) -> bool {
3065        self.attached_databases
3066            .read()
3067            .name_to_index
3068            .contains_key(alias)
3069    }
3070
3071    /// Returns the reserved-space value inherited from the main connection's pager.
3072    /// (This reads the main database pager, not the pager of db to be attached)
3073    fn inherited_reserved_space_for_fresh_attach(&self) -> u8 {
3074        let pager = self.pager.load();
3075        pager
3076            .get_reserved_space()
3077            .unwrap_or_else(|| pager.io_ctx.read().get_reserved_space_bytes())
3078    }
3079
3080    /// Returns the minimum reserved space required by the attached pager's own IO context.
3081    /// This is used as a floor so inherited or explicit values cannot undercut the attached DB.
3082    fn minimum_reserved_space_for_fresh_attach(pager: &Pager) -> u8 {
3083        pager
3084            .get_reserved_space()
3085            .unwrap_or(0)
3086            .max(pager.io_ctx.read().get_reserved_space_bytes())
3087    }
3088
3089    fn database_has_existing_wal_state(db: &Database) -> bool {
3090        let shared_wal = db.shared_wal.read();
3091        shared_wal.page_size() != 0 || shared_wal.last_checksum_and_max_frame().1 != 0
3092    }
3093
3094    fn install_database_wal_on_pager(db: &Arc<Database>, pager: &mut Arc<Pager>) {
3095        let shared_wal = db.shared_wal.clone();
3096        let last_checksum_and_max_frame = shared_wal.read().last_checksum_and_max_frame();
3097        let wal = Arc::new(crate::storage::wal::WalFile::new(
3098            db.io.clone(),
3099            shared_wal,
3100            last_checksum_and_max_frame,
3101            db.buffer_pool.clone(),
3102        ));
3103
3104        let pager = Arc::get_mut(pager)
3105            .expect("fresh attached pager must not be shared before bootstrap or publication");
3106        pager.set_wal(wal);
3107    }
3108
3109    fn set_mvcc_journal_mode_fresh_db(pager: &Pager) -> Result<()> {
3110        turso_assert!(!pager.db_initialized());
3111        pager.set_initial_journal_version(crate::storage::sqlite3_ondisk::Version::Mvcc)
3112    }
3113
3114    fn validate_attach_target(db: &Database, is_fresh: bool, alias: &str) -> Result<()> {
3115        if is_fresh && Self::database_has_existing_wal_state(db) {
3116            return Err(LimboError::InvalidArgument(format!(
3117                "cannot attach database '{alias}': main database file is uninitialized but WAL state exists"
3118            )));
3119        }
3120
3121        if is_fresh && db.is_readonly() {
3122            return Err(LimboError::InvalidArgument(format!(
3123                "cannot attach database '{alias}': fresh read-only databases cannot be initialized during attach"
3124            )));
3125        }
3126        Ok(())
3127    }
3128
3129    fn apply_page_layout_to_fresh_attach_db(
3130        &self,
3131        alias: &str,
3132        attached_db_pager: &Pager,
3133        reserved_space: Option<u8>,
3134    ) -> Result<()> {
3135        let target_page_size = self.get_page_size();
3136        let attached_min_reserved_space =
3137            Self::minimum_reserved_space_for_fresh_attach(attached_db_pager);
3138        let target_reserved_space = match reserved_space {
3139            Some(space) => {
3140                // this happens reserved_space is explicitly passed along with encryption or checksum
3141                if space < attached_min_reserved_space {
3142                    return Err(LimboError::InvalidArgument(format!(
3143                        "cannot attach database '{alias}': reserved space {space} is smaller than attached database minimum {attached_min_reserved_space}"
3144                    )));
3145                }
3146                Some(space)
3147            }
3148            None => Some(
3149                self.inherited_reserved_space_for_fresh_attach()
3150                    .max(attached_min_reserved_space),
3151            ),
3152        };
3153
3154        attached_db_pager.set_initial_page_size(target_page_size)?;
3155        if let Some(reserved_space) = target_reserved_space {
3156            attached_db_pager.set_reserved_space_bytes(reserved_space);
3157        }
3158        Ok(())
3159    }
3160
3161    fn reject_initialized_attach_mismatches(
3162        &self,
3163        alias: &str,
3164        db: &Database,
3165        pager: &Pager,
3166    ) -> Result<()> {
3167        // Reject incompatible journal modes for initialized attached databases:
3168        // we cannot silently convert the header (the user may have attached read-only).
3169        if self.mvcc_enabled() != db.mvcc_enabled() {
3170            let main_mode = if self.mvcc_enabled() { "MVCC" } else { "WAL" };
3171            let attached_mode = if db.mvcc_enabled() { "MVCC" } else { "WAL" };
3172            return Err(LimboError::InvalidArgument(format!(
3173                "cannot attach database '{alias}': main database uses {main_mode} journal mode \
3174                 but attached database uses {attached_mode}. Both must use the same journal mode."
3175            )));
3176        }
3177
3178        // Reject mismatched page sizes: ephemeral tables and cross-database
3179        // operations assume a uniform page size across all attached databases.
3180        let main_pager = self.pager.load();
3181        if let (Some(main_ps), Some(attached_ps)) =
3182            (main_pager.get_page_size(), pager.get_page_size())
3183        {
3184            if main_ps != attached_ps {
3185                return Err(LimboError::InvalidArgument(format!(
3186                    "cannot attach database '{alias}': page size mismatch \
3187                     (main={main_ps:?}, attached={attached_ps:?})"
3188                )));
3189            }
3190        }
3191
3192        Ok(())
3193    }
3194
3195    fn reject_unsupported_fresh_mvcc_attach_durable_storage(
3196        &self,
3197        alias: &str,
3198        db: &Database,
3199        attached_is_fresh: bool,
3200    ) -> Result<()> {
3201        if attached_is_fresh
3202            && self.mvcc_enabled()
3203            && self.db.durable_storage.is_some()
3204            && db.durable_storage.is_none()
3205        {
3206            return Err(LimboError::InvalidArgument(format!(
3207                "cannot attach database '{alias}': fresh MVCC attach does not support inheriting custom durable storage"
3208            )));
3209        }
3210
3211        Ok(())
3212    }
3213
3214    /// Attach a database file with the given alias name
3215    #[cfg(not(clt_turso_feature = "fs"))]
3216    pub(crate) fn attach_database(
3217        &self,
3218        _path: &str,
3219        _alias: &str,
3220        _state: &mut AttachDatabaseState,
3221    ) -> Result<IOResult<()>> {
3222        Err(LimboError::InvalidArgument(
3223            "attach not available in this build (no-fs)".to_string(),
3224        ))
3225    }
3226
3227    #[cfg(not(clt_turso_feature = "fs"))]
3228    pub(crate) fn attach_database_with_config(
3229        &self,
3230        _path: &str,
3231        _alias: &str,
3232        _reserved_space: Option<u8>,
3233        _state: &mut AttachDatabaseState,
3234    ) -> Result<IOResult<()>> {
3235        // File-backed ATTACH is unavailable without `fs`, so pre-initialization
3236        // page-layout overrides are also unsupported in this build.
3237        self.attach_database(_path, _alias, _state)
3238    }
3239
3240    /// Attach a database file with the given alias name
3241    #[cfg(clt_turso_feature = "fs")]
3242    pub(crate) fn attach_database(
3243        &self,
3244        path: &str,
3245        alias: &str,
3246        state: &mut AttachDatabaseState,
3247    ) -> Result<IOResult<()>> {
3248        self.attach_database_with_config(path, alias, None, state)
3249    }
3250
3251    /// Attach a database file with an optional pre-initialization reserved-space override.
3252    #[cfg(clt_turso_feature = "fs")]
3253    #[cfg_attr(not(clt_turso_tests), allow(dead_code))]
3254    pub(crate) fn attach_database_with_config(
3255        &self,
3256        path: &str,
3257        alias: &str,
3258        reserved_space: Option<u8>,
3259        state: &mut AttachDatabaseState,
3260    ) -> Result<IOResult<()>> {
3261        loop {
3262            match state {
3263                AttachDatabaseState::Start => {
3264                    if self.is_closed() {
3265                        return Err(LimboError::InternalError("Connection closed".to_string()));
3266                    }
3267
3268                    if self.is_attached(alias) {
3269                        return Err(LimboError::InvalidArgument(format!(
3270                            "database {alias} is already in use"
3271                        )));
3272                    }
3273
3274                    if alias.eq_ignore_ascii_case("main") || alias.eq_ignore_ascii_case("temp") {
3275                        return Err(LimboError::InvalidArgument(format!(
3276                            "reserved name {alias} is already in use"
3277                        )));
3278                    }
3279
3280                    let db_opts = DatabaseOpts::new()
3281                        .with_views(self.db.experimental_views_enabled())
3282                        .with_custom_types(self.db.experimental_custom_types_enabled())
3283                        .with_index_method(self.db.experimental_index_method_enabled())
3284                        .with_vacuum(self.db.experimental_vacuum_enabled())
3285                        .with_generated_columns(self.db.experimental_generated_columns_enabled())
3286                        .with_without_rowid(self.db.experimental_without_rowid_enabled());
3287                    let is_memory_db = is_memory_like(path);
3288                    let io: Arc<dyn IO> = if is_memory_db {
3289                        Arc::new(MemoryIO::new())
3290                    } else if self.db.is_in_memory_db() {
3291                        Database::io_for_path(path)?
3292                    } else {
3293                        self.db.io.clone()
3294                    };
3295                    let main_db_flags = self.db.open_flags;
3296                    let (db, encryption_opts) =
3297                        Self::from_uri_attached(path, db_opts, main_db_flags, io)?;
3298                    let attached_is_fresh = !db.initialized();
3299                    if !is_memory_db {
3300                        Self::validate_attach_target(&db, attached_is_fresh, alias)?;
3301                    }
3302                    self.reject_unsupported_fresh_mvcc_attach_durable_storage(
3303                        alias,
3304                        &db,
3305                        attached_is_fresh,
3306                    )?;
3307
3308                    let encryption_key = if let Some(ref enc) = encryption_opts {
3309                        Some(EncryptionKey::from_hex_string(&enc.hexkey)?)
3310                    } else {
3311                        None
3312                    };
3313
3314                    *state = AttachDatabaseState::Init(Box::new(AttachDatabaseInitState {
3315                        alias: alias.to_string(),
3316                        reserved_space,
3317                        db,
3318                        attached_is_fresh,
3319                        encryption_key,
3320                        init_st: crate::InitState::default(),
3321                    }));
3322                }
3323                AttachDatabaseState::Init(init) => {
3324                    let mut pager = Arc::new(crate::return_if_io!(init
3325                        .db
3326                        ._init_nonblock(&mut init.init_st, init.encryption_key.as_ref(),)));
3327
3328                    if !init.attached_is_fresh {
3329                        self.reject_initialized_attach_mismatches(&init.alias, &init.db, &pager)?;
3330                        *state = AttachDatabaseState::Publish {
3331                            alias: init.alias.clone(),
3332                            db: init.db.clone(),
3333                            pager,
3334                        };
3335                        continue;
3336                    }
3337
3338                    self.apply_page_layout_to_fresh_attach_db(
3339                        &init.alias,
3340                        &pager,
3341                        init.reserved_space,
3342                    )?;
3343
3344                    if self.mvcc_enabled() && !init.db.mvcc_enabled() {
3345                        Self::set_mvcc_journal_mode_fresh_db(&pager)?;
3346                        Self::install_database_wal_on_pager(&init.db, &mut pager);
3347                        let enc_ctx = pager.io_ctx.read().encryption_context().cloned();
3348                        let mv_store = journal_mode::open_mv_store(
3349                            init.db.io.clone(),
3350                            &init.db.path,
3351                            init.db.open_flags,
3352                            init.db.durable_storage.clone(),
3353                            enc_ctx,
3354                            init.db.mv_store_allocator.clone(),
3355                            init.db.experimental_mvcc_passive_checkpoint_enabled(),
3356                        )?;
3357                        init.db.mv_store.store(Some(mv_store));
3358                        *state = AttachDatabaseState::Bootstrap(Box::new(
3359                            AttachDatabaseBootstrapState {
3360                                alias: init.alias.clone(),
3361                                db: init.db.clone(),
3362                                pager,
3363                                encryption_key: init.encryption_key.take(),
3364                                bootstrap_conn: None,
3365                                bootstrap_st: crate::mvcc::database::BootstrapState::default(),
3366                            },
3367                        ));
3368                    } else {
3369                        *state = AttachDatabaseState::Publish {
3370                            alias: init.alias.clone(),
3371                            db: init.db.clone(),
3372                            pager,
3373                        };
3374                    }
3375                }
3376                AttachDatabaseState::Bootstrap(bootstrap) => {
3377                    if bootstrap.bootstrap_conn.is_none() {
3378                        let default_cache_size = match bootstrap
3379                            .pager
3380                            .with_header(|header| header.default_page_cache_size)
3381                        {
3382                            Ok(IOResult::Done(default_cache_size)) => default_cache_size.get(),
3383                            Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
3384                            Err(_) => 0,
3385                        };
3386                        bootstrap.bootstrap_conn =
3387                            Some(bootstrap.db._connect_with_pager_and_default_cache_size(
3388                                true,
3389                                bootstrap.pager.clone(),
3390                                bootstrap.encryption_key.take(),
3391                                default_cache_size,
3392                            )?);
3393                    }
3394
3395                    let mv_store_guard = bootstrap.db.get_mv_store();
3396                    let Some(mv_store) = mv_store_guard.as_ref() else {
3397                        return Err(LimboError::InternalError(
3398                            "fresh MVCC attach missing MV store".to_string(),
3399                        ));
3400                    };
3401                    crate::return_if_io!(mv_store.bootstrap_nonblock(
3402                        bootstrap
3403                            .bootstrap_conn
3404                            .as_ref()
3405                            .expect("bootstrap connection initialized above"),
3406                        &mut bootstrap.bootstrap_st,
3407                    ));
3408
3409                    *state = AttachDatabaseState::Publish {
3410                        alias: bootstrap.alias.clone(),
3411                        db: bootstrap.db.clone(),
3412                        pager: bootstrap.pager.clone(),
3413                    };
3414                }
3415                AttachDatabaseState::Publish { alias, db, pager } => {
3416                    self.attached_databases
3417                        .write()
3418                        .insert(alias.as_str(), (db.clone(), pager.clone()));
3419                    self.bump_prepare_context_generation();
3420                    *state = AttachDatabaseState::Done;
3421                    return Ok(IOResult::Done(()));
3422                }
3423                AttachDatabaseState::Done => {
3424                    return Err(LimboError::InternalError(
3425                        "attach_database called after completion".to_string(),
3426                    ));
3427                }
3428            }
3429        }
3430    }
3431
3432    // Detach a database by alias name
3433    pub(crate) fn detach_database(&self, alias: &str) -> Result<()> {
3434        if self.is_closed() {
3435            return Err(LimboError::InternalError("Connection closed".to_string()));
3436        }
3437
3438        if alias == "main" || alias == "temp" {
3439            return Err(LimboError::InvalidArgument(format!(
3440                "cannot detach database: {alias}"
3441            )));
3442        }
3443
3444        // Look up the database index first, then rollback any MVCC transaction
3445        // *before* removing the database from the catalog.  mv_store_for_db
3446        // and get_pager_from_database_index read `attached_databases`, so we
3447        // must not hold the write lock during the rollback.
3448        let database_id = {
3449            let attached_dbs = self.attached_databases.read();
3450            match attached_dbs.name_to_index.get(alias).copied() {
3451                Some(id) => id,
3452                None => {
3453                    return Err(LimboError::InvalidArgument(format!(
3454                        "no such database: {alias}"
3455                    )));
3456                }
3457            }
3458        };
3459
3460        // Rollback any active transaction on this database before detaching.
3461        // After the Database is removed from the catalog, the MvStore / Pager
3462        // become unreachable and the transaction would leak forever.
3463        let pager = self
3464            .get_pager_from_database_index(&database_id)
3465            .expect("attached database should always have a pager");
3466
3467        if pager.holds_read_lock() || pager.holds_write_lock() {
3468            return Err(LimboError::InvalidArgument(format!(
3469                "database {alias} is locked"
3470            )));
3471        }
3472
3473        if let Some((tx_id, _mode)) = self.get_mv_tx_for_db(database_id) {
3474            if let Some(mv_store) = self.mv_store_for_db(database_id) {
3475                mv_store.rollback_tx(tx_id, pager.clone(), self, database_id);
3476                pager.end_read_tx();
3477            }
3478            self.set_mv_tx_for_db(database_id, None);
3479        } else {
3480            // Non-MVCC attached DB (e.g. :memory:) — rollback WAL state.
3481            pager.rollback_attached();
3482        }
3483
3484        // Remove from catalog. The write lock must be released before
3485        // acquiring database_schemas.write() to maintain consistent lock
3486        // ordering (attached_databases before database_schemas).
3487        {
3488            let mut attached_dbs = self.attached_databases.write();
3489            attached_dbs.remove(alias);
3490        }
3491
3492        // Invalidate the cached schema for this database index so that a future
3493        // ATTACH reusing the same index won't see stale schema entries.
3494        self.database_schemas.write().remove(&database_id);
3495        self.bump_prepare_context_generation();
3496
3497        Ok(())
3498    }
3499
3500    /// List all attached database aliases
3501    pub fn list_attached_databases(&self) -> Vec<String> {
3502        self.attached_databases
3503            .read()
3504            .name_to_index
3505            .keys()
3506            .cloned()
3507            .collect()
3508    }
3509
3510    /// Invoke `f` with a slice of all non-main database (index, pager) pairs
3511    /// (temp + attached).The internal locks are released before `f` runs, which also
3512    /// makes it safe for `f` to call back into the connection (e.g. `mv_store_for_db`,
3513    /// which re-reads the attached-database catalog).
3514    pub(crate) fn with_all_attached_pagers_with_index<F, R>(&self, f: F) -> R
3515    where
3516        F: FnOnce(&[(usize, Arc<Pager>)]) -> R,
3517    {
3518        let mut pagers: SmallVec<[(usize, Arc<Pager>); 8]> = SmallVec::new();
3519        if let Some(temp_db) = self.temp.database.read().as_ref() {
3520            pagers.push((crate::TEMP_DB_ID, temp_db.pager.clone()));
3521        }
3522        {
3523            let catalog = self.attached_databases.read();
3524            for (&idx, (_db, pager)) in catalog.index_to_data.iter() {
3525                pagers.push((idx, pager.clone()));
3526            }
3527        }
3528        f(&pagers)
3529    }
3530
3531    pub(crate) fn database_schemas(&self) -> &RwLock<HashMap<usize, Arc<Schema>>> {
3532        &self.database_schemas
3533    }
3534
3535    fn cached_non_main_schema(&self, database_id: usize) -> Arc<Schema> {
3536        turso_assert_ne!(database_id, crate::MAIN_DB_ID);
3537        // TEMP is the sole source-of-truth path: writes go directly to
3538        // `temp_db.db.schema` (see `with_database_schema_mut`), so skip
3539        // `database_schemas` entirely to avoid stale reads.
3540        if database_id == crate::TEMP_DB_ID {
3541            return self
3542                .temp
3543                .database
3544                .read()
3545                .as_ref()
3546                .map(|temp_db| temp_db.db.schema.lock().clone())
3547                .unwrap_or_else(|| self.empty_temp_schema());
3548        }
3549        if let Some(schema) = self.database_schemas.read().get(&database_id).cloned() {
3550            return schema;
3551        }
3552
3553        let attached_dbs = self.attached_databases.read();
3554        let (db, _pager) = attached_dbs
3555            .index_to_data
3556            .get(&database_id)
3557            .expect("Database ID should be valid after resolve_database_id");
3558        let schema = db.schema.lock().clone();
3559        schema
3560    }
3561
3562    /// Publish a connection-local non-main schema after commit.
3563    ///
3564    /// TEMP is not staged in `database_schemas` — writes go directly to
3565    /// `temp_db.db.schema` via `with_database_schema_mut`, so there is
3566    /// nothing to publish here. Attached databases still stage mutations
3567    /// in `database_schemas` so other connections don't see uncommitted
3568    /// DDL; those get published to the shared `db.schema` on commit.
3569    pub(crate) fn publish_database_schema(&self, database_id: usize) {
3570        if database_id == crate::TEMP_DB_ID {
3571            return;
3572        }
3573        let mut schemas = self.database_schemas.write();
3574        if let Some(local_schema) = schemas.remove(&database_id) {
3575            let attached_dbs = self.attached_databases.read();
3576            if let Some((db, _pager)) = attached_dbs.index_to_data.get(&database_id) {
3577                *db.schema.lock() = local_schema;
3578            }
3579            self.bump_prepare_context_generation();
3580        }
3581    }
3582
3583    pub(crate) fn attached_databases(&self) -> &RwLock<DatabaseCatalog> {
3584        &self.attached_databases
3585    }
3586
3587    /// Access schema for a database using a closure pattern to avoid cloning
3588    pub(crate) fn with_schema<T>(&self, database_id: usize, f: impl FnOnce(&Schema) -> T) -> T {
3589        match database_id {
3590            crate::MAIN_DB_ID => {
3591                let schema = self.schema.read();
3592                f(&schema)
3593            }
3594            _ => {
3595                let schema = self.cached_non_main_schema(database_id);
3596                f(&schema)
3597            }
3598        }
3599    }
3600
3601    /// Clone the *shared* schema of `database_id` (main or attached), bypassing
3602    /// the per-connection schema cache. Falls back to the main DB's shared
3603    /// schema when `database_id` does not name an attached database — callers
3604    /// in error paths get something usable instead of a panic.
3605    ///
3606    /// MVCC checkpoint specifically must call this rather than [`Self::with_schema`]:
3607    /// it writes from the mv store to the pager, so the schema it uses must
3608    /// match the pager being checkpointed and cannot be a stale per-connection
3609    /// copy.
3610    pub(crate) fn clone_shared_schema(&self, database_id: usize) -> Arc<Schema> {
3611        if database_id == crate::MAIN_DB_ID {
3612            self.db.clone_schema()
3613        } else {
3614            self.attached_databases
3615                .read()
3616                .index_to_data
3617                .get(&database_id)
3618                .map(|(db, _)| db.schema.lock().clone())
3619                .unwrap_or_else(|| self.db.clone_schema())
3620        }
3621    }
3622
3623    // Get the canonical path for a database given its Database object
3624    fn get_canonical_path_for_database(db: &Database) -> String {
3625        if db.is_in_memory_db() {
3626            // For in-memory databases, SQLite shows empty string
3627            String::new()
3628        } else {
3629            // For file databases, try to show the full absolute path if that doesn't fail
3630            match std::fs::canonicalize(&db.path) {
3631                Ok(abs_path) => abs_path.to_string_lossy().to_string(),
3632                Err(_) => db.path.to_string(),
3633            }
3634        }
3635    }
3636
3637    /// List all databases (main + attached) with their sequence numbers, names, and file paths
3638    /// Returns a vector of tuples: (seq_number, name, file_path)
3639    pub fn list_all_databases(&self) -> Vec<(usize, String, String)> {
3640        let mut databases = Vec::new();
3641
3642        // Add main database (always seq=0, name="main")
3643        let main_path = Self::get_canonical_path_for_database(&self.db);
3644        databases.push((MAIN_DB_ID, "main".to_string(), main_path));
3645
3646        // SQLite only exposes the temp schema in database_list after it has
3647        // been initialized, and reports an empty path rather than the backing
3648        // temp filename.
3649        if self.temp.database.read().is_some() {
3650            databases.push((crate::TEMP_DB_ID, "temp".to_string(), String::new()));
3651        }
3652
3653        // Add attached databases
3654        let attached_dbs = self.attached_databases.read();
3655        for (alias, &seq_number) in attached_dbs.name_to_index.iter() {
3656            let file_path = if let Some((db, _pager)) = attached_dbs.index_to_data.get(&seq_number)
3657            {
3658                Self::get_canonical_path_for_database(db)
3659            } else {
3660                String::new()
3661            };
3662            databases.push((seq_number, alias.clone(), file_path));
3663        }
3664
3665        // Sort by sequence number to ensure consistent ordering
3666        databases.sort_by_key(|&(seq, _, _)| seq);
3667        databases
3668    }
3669
3670    pub fn get_pager(&self) -> Arc<Pager> {
3671        self.pager.load().clone()
3672    }
3673
3674    pub fn get_query_only(&self) -> bool {
3675        self.is_query_only()
3676    }
3677
3678    pub fn set_query_only(&self, value: bool) {
3679        self.query_only.store(value, Ordering::SeqCst);
3680        self.bump_prepare_context_generation();
3681    }
3682
3683    pub fn set_vdbe_trace(&self, value: bool) {
3684        self.vdbe_trace.store(value, Ordering::SeqCst);
3685    }
3686
3687    pub fn get_vdbe_trace(&self) -> bool {
3688        self.vdbe_trace.load(Ordering::SeqCst)
3689    }
3690
3691    pub fn get_dml_require_where(&self) -> bool {
3692        self.dml_require_where.load(Ordering::SeqCst)
3693    }
3694
3695    pub fn set_dml_require_where(&self, value: bool) {
3696        self.dml_require_where.store(value, Ordering::SeqCst);
3697    }
3698
3699    pub fn get_dqs_dml(&self) -> bool {
3700        self.dqs_dml.load(Ordering::SeqCst)
3701    }
3702
3703    pub fn set_dqs_dml(&self, value: bool) {
3704        self.dqs_dml.store(value, Ordering::SeqCst);
3705        self.bump_prepare_context_generation();
3706    }
3707
3708    pub fn get_full_column_names(&self) -> bool {
3709        self.full_column_names.load(Ordering::SeqCst)
3710    }
3711
3712    pub fn set_full_column_names(&self, value: bool) {
3713        self.full_column_names.store(value, Ordering::SeqCst);
3714        self.bump_prepare_context_generation();
3715    }
3716
3717    pub fn get_short_column_names(&self) -> bool {
3718        self.short_column_names.load(Ordering::SeqCst)
3719    }
3720
3721    pub fn set_short_column_names(&self, value: bool) {
3722        self.short_column_names.store(value, Ordering::SeqCst);
3723        self.bump_prepare_context_generation();
3724    }
3725
3726    pub fn get_sync_mode(&self) -> SyncMode {
3727        self.sync_mode.get()
3728    }
3729
3730    pub fn set_sync_mode(&self, mode: SyncMode) {
3731        self.sync_mode.set(mode);
3732        self.bump_prepare_context_generation();
3733    }
3734
3735    pub fn get_temp_store(&self) -> crate::TempStore {
3736        self.temp_store.get()
3737    }
3738
3739    pub fn set_temp_store(&self, value: crate::TempStore) {
3740        if self.temp_store.get() == value {
3741            return;
3742        }
3743        self.reset_temp_database();
3744        self.temp_store.set(value);
3745        self.bump_prepare_context_generation();
3746    }
3747
3748    /// Find a sequence by name, supporting optional schema qualification.
3749    ///
3750    /// - `"my_seq"` → searches main database only
3751    /// - `"aux.my_seq"` → searches the attached database named `aux`
3752    pub fn find_sequence(&self, name: &str) -> Result<Arc<crate::schema::Sequence>> {
3753        let (db_id, seq_name) = if let Some((schema, seq)) = name.split_once('.') {
3754            let db_id = self.get_database_id_by_name(schema)?;
3755            (db_id, crate::util::normalize_ident(seq))
3756        } else {
3757            (MAIN_DB_ID, crate::util::normalize_ident(name))
3758        };
3759
3760        self.with_schema(db_id, |schema| {
3761            schema.get_sequence(&seq_name).map(Arc::clone)
3762        })
3763        .ok_or_else(|| LimboError::ParseError(format!("sequence \"{name}\" does not exist")))
3764    }
3765
3766    /// Record that this connection has seen a value from the named sequence (for currval).
3767    pub fn set_sequence_currval(&self, name: &str, value: i64) {
3768        let normalized = crate::util::normalize_ident(name);
3769        self.sequence_currvals.write().insert(normalized, value);
3770    }
3771
3772    /// Get the last value returned by nextval/setval for the named sequence on this connection.
3773    pub fn get_sequence_currval(&self, name: &str) -> Option<i64> {
3774        let normalized = crate::util::normalize_ident(name);
3775        self.sequence_currvals.read().get(&normalized).copied()
3776    }
3777
3778    /// Drop this connection's currval entry for a sequence. Called on DROP
3779    /// SEQUENCE (and implicit drops via DROP TABLE on AUTOINCREMENT) so that
3780    /// a subsequent `CREATE SEQUENCE <same-name>` does not silently inherit
3781    /// the stale per-session currval from the prior sequence — `currval()`
3782    /// on the fresh sequence must error with "not yet defined in this
3783    /// session" until a nextval/setval establishes it.
3784    pub fn clear_sequence_currval(&self, name: &str) {
3785        let normalized = crate::util::normalize_ident(name);
3786        self.sequence_currvals.write().remove(&normalized);
3787    }
3788
3789    /// Total times this connection's autonomous sequence inner-tx ran into
3790    /// a transient conflict (`WriteWriteConflict` / `BusySnapshot` /
3791    /// `Conflict(_)`) and was retried by `op_sequence_commit_inner_tx`.
3792    /// A non-CYCLE nextval on a non-contended seq must keep this at zero —
3793    /// the regression test for "no inline backing-table compaction"
3794    /// asserts the delta is 0 across the concurrent-nextval scenario.
3795    pub fn sequence_inner_retries(&self) -> u64 {
3796        self.sequence_inner_retries
3797            .load(std::sync::atomic::Ordering::Relaxed)
3798    }
3799
3800    /// Reset the inner-tx retry counter. Test-only helper so a setup
3801    /// phase (priming the backing table, etc.) doesn't pollute the
3802    /// counter the assertion phase observes.
3803    #[doc(hidden)]
3804    pub fn reset_sequence_inner_retries(&self) {
3805        self.sequence_inner_retries
3806            .store(0, std::sync::atomic::Ordering::Relaxed);
3807    }
3808
3809    /// Bootstrap-time sequence descriptor loader. Used by MVCC bootstrap
3810    /// after log recovery: walks `__turso_internal_seq_*` tables and registers
3811    /// a pure descriptor for each into the active schema. No atomic state is
3812    /// seeded — the runtime watermark is always read from disk by
3813    /// nextval/setval.
3814    ///
3815    /// Non-blocking: driven by the bootstrap state machine via `return_if_io!`,
3816    /// so the per-backing-table descriptor read yields IO rather than pumping
3817    /// `io.step()`. Re-entrant — the worklist and in-flight read live in
3818    /// `state`.
3819    pub(crate) fn load_sequence_descriptors_via_sql_nonblock(
3820        self: &Arc<Connection>,
3821        state: &mut LoadSequenceDescriptorsState,
3822    ) -> Result<crate::types::IOResult<()>> {
3823        use crate::types::IOResult;
3824        loop {
3825            match state {
3826                LoadSequenceDescriptorsState::Start => {
3827                    // Walk schema.tables in-memory rather than issuing a SELECT
3828                    // against sqlite_master — avoids the side-effects of running
3829                    // a fresh statement here, which can leave the connection's
3830                    // mv_tx in a non-exclusive state and cause the next DDL to
3831                    // trip the exclusive-tx guard in op_open_write.
3832                    let pending =
3833                        self.with_schema(MAIN_DB_ID, |s| s.sequence_backing_table_names());
3834                    *state = LoadSequenceDescriptorsState::Reading {
3835                        pending,
3836                        idx: 0,
3837                        stmt: None,
3838                        meta: None,
3839                        seq: None,
3840                        watermark_stmt: None,
3841                        watermark_row: None,
3842                    };
3843                }
3844                LoadSequenceDescriptorsState::Reading {
3845                    pending,
3846                    idx,
3847                    stmt,
3848                    meta,
3849                    seq,
3850                    watermark_stmt,
3851                    watermark_row,
3852                } => loop {
3853                    let entry = {
3854                        if *idx >= pending.len() {
3855                            return Ok(IOResult::Done(()));
3856                        }
3857                        pending[*idx].clone()
3858                    };
3859                    let (backing_table_name, seq_name) = entry;
3860                    let normalized = crate::util::normalize_ident(&seq_name);
3861                    let already_present =
3862                        self.with_schema(MAIN_DB_ID, |s| s.get_sequence(&normalized).is_some());
3863                    if already_present {
3864                        *idx += 1;
3865                        *stmt = None;
3866                        *meta = None;
3867                        *seq = None;
3868                        *watermark_stmt = None;
3869                        *watermark_row = None;
3870                        continue;
3871                    }
3872                    if seq.is_none() {
3873                        crate::return_if_io!(self.read_seq_descriptor_row_nonblock(
3874                            &backing_table_name,
3875                            &seq_name,
3876                            stmt,
3877                            meta,
3878                        ));
3879                        *seq = Some(Self::sequence_from_descriptor_meta(
3880                            &seq_name,
3881                            &backing_table_name,
3882                            *meta,
3883                        )?);
3884                        *stmt = None;
3885                        *meta = None;
3886                    }
3887                    let sequence = seq.as_ref().expect("sequence set above");
3888                    crate::return_if_io!(self.read_sequence_watermark_row_nonblock(
3889                        &backing_table_name,
3890                        sequence,
3891                        watermark_stmt,
3892                        watermark_row,
3893                    ));
3894                    let watermark = Self::sequence_watermark_from_row(
3895                        &backing_table_name,
3896                        sequence,
3897                        *watermark_row,
3898                    )?;
3899                    if let Some(mv_store) = self.db.get_mv_store().as_ref() {
3900                        mv_store.set_sequence_watermark(&normalized, watermark);
3901                    }
3902                    let sequence = seq.take().expect("sequence set above");
3903                    self.with_database_schema_mut(MAIN_DB_ID, |schema| {
3904                        schema
3905                            .sequences
3906                            .insert(normalized.clone(), Arc::new(sequence));
3907                    })?;
3908                    *idx += 1;
3909                    *stmt = None;
3910                    *meta = None;
3911                    *watermark_stmt = None;
3912                    *watermark_row = None;
3913                },
3914            }
3915        }
3916    }
3917
3918    /// Drive one backing-table descriptor read to completion (re-entrant).
3919    /// Lazily prepares the `SELECT` into `*stmt`, then runs it non-blocking,
3920    /// stashing the captured row in `*meta`. The backing table is internal
3921    /// (`__turso_internal_seq_*`); a prepare/read failure is on-disk
3922    /// corruption, not "the sequence doesn't exist", so it surfaces
3923    /// `LimboError::Corrupt` — silently dropping the sequence would manifest
3924    /// as a misleading "sequence does not exist" error on the next nextval
3925    /// that masks the real problem.
3926    fn read_seq_descriptor_row_nonblock(
3927        self: &Arc<Connection>,
3928        backing_table_name: &str,
3929        seq_name: &str,
3930        stmt: &mut Option<Box<Statement>>,
3931        meta: &mut Option<(i64, i64, i64, i64, bool)>,
3932    ) -> Result<crate::types::IOResult<()>> {
3933        use crate::types::IOResult;
3934        if stmt.is_none() {
3935            let escaped = backing_table_name.replace('"', "\"\"");
3936            let sql = format!("SELECT start, inc, min, max, cycle FROM \"{escaped}\" LIMIT 1");
3937            let prepared = self.prepare_internal(sql).map_err(|err| {
3938                LimboError::Corrupt(format!(
3939                    "internal sequence backing table \"{backing_table_name}\" for sequence \
3940                     \"{seq_name}\": cannot prepare descriptor SELECT: {err}"
3941                ))
3942            })?;
3943            *stmt = Some(Box::new(prepared));
3944            // Fresh statement → clear any descriptor captured for a prior backing
3945            // table, so an empty backing table is detected as missing-row
3946            // corruption rather than silently reusing the previous descriptor.
3947            *meta = None;
3948        }
3949        let s = stmt.as_mut().expect("stmt set above");
3950        match s.run_with_row_callback_nonblock(|row| {
3951            *meta = Some((
3952                row.get::<i64>(0)?,
3953                row.get::<i64>(1)?,
3954                row.get::<i64>(2)?,
3955                row.get::<i64>(3)?,
3956                row.get::<i64>(4)? != 0,
3957            ));
3958            Ok(())
3959        }) {
3960            Ok(IOResult::IO(io)) => Ok(IOResult::IO(io)),
3961            Ok(IOResult::Done(())) => Ok(IOResult::Done(())),
3962            Err(err) => Err(LimboError::Corrupt(format!(
3963                "internal sequence backing table \"{backing_table_name}\" for sequence \
3964                 \"{seq_name}\": descriptor row read failed: {err}"
3965            ))),
3966        }
3967    }
3968
3969    /// Build a `Sequence` from a descriptor row captured by
3970    /// [`Self::read_seq_descriptor_row_nonblock`]. An absent/invalid descriptor
3971    /// is on-disk corruption (see that method's doc).
3972    fn sequence_from_descriptor_meta(
3973        seq_name: &str,
3974        backing_table_name: &str,
3975        meta: Option<(i64, i64, i64, i64, bool)>,
3976    ) -> Result<crate::schema::Sequence> {
3977        let (start, inc, min, max, cycle) = meta.ok_or_else(|| {
3978            LimboError::Corrupt(format!(
3979                "internal sequence backing table \"{backing_table_name}\" for sequence \
3980                 \"{seq_name}\" is empty; the descriptor metadata row must always be present"
3981            ))
3982        })?;
3983        crate::schema::Sequence::new(
3984            seq_name.to_string(),
3985            Some(start),
3986            Some(inc),
3987            Some(min),
3988            Some(max),
3989            cycle,
3990        )
3991        .map_err(|err| {
3992            LimboError::Corrupt(format!(
3993                "internal sequence backing table \"{backing_table_name}\" for sequence \
3994                 \"{seq_name}\" descriptor is invalid: {err}"
3995            ))
3996        })
3997    }
3998
3999    /// Drive one backing-table watermark read to completion (re-entrant).
4000    ///
4001    /// The returned row is converted by [`Self::sequence_watermark_from_row`]
4002    /// into the exclusive upper bound used by `sequence_watermark_experimental()`.
4003    fn read_sequence_watermark_row_nonblock(
4004        self: &Arc<Connection>,
4005        backing_table_name: &str,
4006        seq: &crate::schema::Sequence,
4007        stmt: &mut Option<Box<Statement>>,
4008        row: &mut Option<(i64, bool)>,
4009    ) -> Result<crate::types::IOResult<()>> {
4010        use crate::types::IOResult;
4011        if stmt.is_none() {
4012            let escaped = backing_table_name.replace('"', "\"\"");
4013            let direction = if seq.increment_by >= 0 { "DESC" } else { "ASC" };
4014            let sql = format!(
4015                "SELECT value, is_called FROM \"{escaped}\" ORDER BY value {direction} LIMIT 1"
4016            );
4017            let prepared = self.prepare_internal(sql).map_err(|err| {
4018                LimboError::Corrupt(format!(
4019                    "internal sequence backing table \"{backing_table_name}\" for sequence \
4020                     \"{}\": cannot prepare watermark SELECT: {err}",
4021                    seq.name
4022                ))
4023            })?;
4024            *stmt = Some(Box::new(prepared));
4025            *row = None;
4026        }
4027        let s = stmt.as_mut().expect("stmt set above");
4028        match s.run_with_row_callback_nonblock(|r| {
4029            let value = r.get::<i64>(0)?;
4030            let is_called = r.get::<i64>(1)? != 0;
4031            *row = Some((value, is_called));
4032            Ok(())
4033        }) {
4034            Ok(IOResult::IO(io)) => Ok(IOResult::IO(io)),
4035            Ok(IOResult::Done(())) => Ok(IOResult::Done(())),
4036            Err(err) => Err(LimboError::Corrupt(format!(
4037                "internal sequence backing table \"{backing_table_name}\" for sequence \
4038                 \"{}\": watermark row read failed: {err}",
4039                seq.name
4040            ))),
4041        }
4042    }
4043
4044    fn sequence_watermark_from_row(
4045        backing_table_name: &str,
4046        seq: &crate::schema::Sequence,
4047        row: Option<(i64, bool)>,
4048    ) -> Result<i64> {
4049        let (value, is_called) = row.ok_or_else(|| {
4050            LimboError::Corrupt(format!(
4051                "internal sequence backing table \"{backing_table_name}\" for sequence \
4052                 \"{}\" is empty; cannot derive sequence watermark",
4053                seq.name
4054            ))
4055        })?;
4056        Ok(crate::mvcc::database::first_unsafe_sequence_watermark(
4057            seq, value, is_called,
4058        ))
4059    }
4060
4061    /// Sync AUTOINCREMENT backing-table watermarks from `sqlite_sequence`.
4062    ///
4063    /// Covers the WAL→MVCC mode-switch compatibility path: a WAL-mode
4064    /// database with AUTOINCREMENT tables tracks the high-water mark in
4065    /// `sqlite_sequence` (legacy SQLite contract) and never writes to
4066    /// the backing table created by CREATE TABLE bytecode. The MVCC
4067    /// AUTOINCREMENT path reads the backing table to compute the next
4068    /// rowid, so without a sync step the next INSERT would regress to
4069    /// start_value and collide with the existing rowid.
4070    ///
4071    /// For each `name` in `sqlite_sequence`, locate the backing table
4072    /// `__turso_internal_seq___turso_internal_autoincrement_<name>` and,
4073    /// if its current MAX(value) is below the sqlite_sequence value,
4074    /// INSERT a new watermark row to advance it. This is the same
4075    /// pattern the translator emits for `emit_disk_advance_past`,
4076    /// expressed as statement-level SQL so it can run at bootstrap.
4077    ///
4078    /// Tables whose backing table is missing are skipped — that
4079    /// indicates the table was never an AUTOINCREMENT under Turso's
4080    /// CREATE TABLE bytecode (i.e. it predates this engine touching
4081    /// the DB), and synthesising a backing table here would forge data
4082    /// the user did not author. Importing a foreign SQLite database is
4083    /// out of scope for this helper.
4084    ///
4085    /// Non-blocking: driven by the bootstrap state machine via `return_if_io!`.
4086    /// Each step is on the correctness path documented above — a silent failure
4087    /// leaves MVCC AUTOINCREMENT able to re-emit a rowid already in use after a
4088    /// WAL→MVCC mode switch, so errors propagate to fail the open rather than
4089    /// continue into a state where the next INSERT NULL collides on disk.
4090    pub(crate) fn sync_autoincrement_backing_tables_from_sqlite_sequence_nonblock(
4091        self: &Arc<Connection>,
4092        state: &mut SyncAutoincrementState,
4093    ) -> Result<crate::types::IOResult<()>> {
4094        use crate::schema::{autoincrement_sequence_name, SQLITE_SEQUENCE_TABLE_NAME};
4095        use crate::translate::sequence::sequence_backing_table_name;
4096        use crate::types::IOResult;
4097
4098        loop {
4099            match state {
4100                SyncAutoincrementState::Start => {
4101                    let has_seq_table = self.with_schema(MAIN_DB_ID, |s| {
4102                        s.get_btree_table(SQLITE_SEQUENCE_TABLE_NAME).is_some()
4103                    });
4104                    if !has_seq_table {
4105                        return Ok(IOResult::Done(()));
4106                    }
4107                    let stmt = self.prepare_internal(format!(
4108                        "SELECT name, seq FROM {SQLITE_SEQUENCE_TABLE_NAME}"
4109                    ))?;
4110                    *state = SyncAutoincrementState::ReadSeqRows {
4111                        stmt: Box::new(stmt),
4112                        rows: Vec::new(),
4113                    };
4114                }
4115                SyncAutoincrementState::ReadSeqRows { stmt, rows } => {
4116                    crate::return_if_io!(stmt.run_with_row_callback_nonblock(|row| {
4117                        let name = row.get::<&str>(0)?.to_string();
4118                        let seq = row.get::<i64>(1)?;
4119                        rows.push((name, seq));
4120                        Ok(())
4121                    }));
4122                    let rows = std::mem::take(rows);
4123                    *state = SyncAutoincrementState::Process {
4124                        rows,
4125                        idx: 0,
4126                        sub: SyncRowStep::Start,
4127                    };
4128                }
4129                SyncAutoincrementState::Process { rows, idx, sub } => {
4130                    if *idx >= rows.len() {
4131                        return Ok(IOResult::Done(()));
4132                    }
4133                    match sub {
4134                        SyncRowStep::Start => {
4135                            let backing_table_name = sequence_backing_table_name(
4136                                &autoincrement_sequence_name(&rows[*idx].0),
4137                            );
4138                            let has_backing = self.with_schema(MAIN_DB_ID, |s| {
4139                                s.get_btree_table(&backing_table_name).is_some()
4140                            });
4141                            if !has_backing {
4142                                *idx += 1;
4143                                continue;
4144                            }
4145                            // Read current backing watermark; only upsert if we'd
4146                            // actually advance it (avoids needless writes on boot).
4147                            let escaped = backing_table_name.replace('"', "\"\"");
4148                            let stmt = self.prepare_internal(format!(
4149                                "SELECT MAX(value) FROM \"{escaped}\""
4150                            ))?;
4151                            *sub = SyncRowStep::ReadMax {
4152                                backing_table_name,
4153                                stmt: Box::new(stmt),
4154                                current_max: None,
4155                            };
4156                        }
4157                        SyncRowStep::ReadMax {
4158                            backing_table_name,
4159                            stmt,
4160                            current_max,
4161                        } => {
4162                            crate::return_if_io!(stmt.run_with_row_callback_nonblock(|row| {
4163                                if let crate::Value::Numeric(crate::Numeric::Integer(v)) =
4164                                    row.get_value(0)
4165                                {
4166                                    *current_max = Some(*v);
4167                                }
4168                                Ok(())
4169                            }));
4170                            let watermark = rows[*idx].1;
4171                            // Skip only when the backing table is already strictly
4172                            // ahead; an equal value is NOT enough because the
4173                            // initial row written by CREATE TABLE bytecode is
4174                            // (value=1, is_called=false), which would cause the
4175                            // next nextval to re-emit value=1 and collide with the
4176                            // rowid already inserted in WAL mode. We always upsert
4177                            // with is_called=1 so the next nextval computes
4178                            // watermark+1 like sqlite_sequence semantics demand.
4179                            if matches!(*current_max, Some(c) if c > watermark) {
4180                                *idx += 1;
4181                                *sub = SyncRowStep::Start;
4182                                continue;
4183                            }
4184                            // Standard AUTOINCREMENT descriptor columns (start=1,
4185                            // inc=1, min=1, max=i64::MAX, cycle=0) — mirror what
4186                            // the translator emits when CREATE TABLE bytecode
4187                            // creates the backing table for an AUTOINCREMENT column.
4188                            let escaped = backing_table_name.replace('"', "\"\"");
4189                            let insert_sql = format!(
4190                                "INSERT OR REPLACE INTO \"{escaped}\"\
4191                                 (value, is_called, start, inc, min, max, cycle) \
4192                                 VALUES ({watermark}, 1, 1, 1, 1, {}, 0)",
4193                                i64::MAX
4194                            );
4195                            let stmt = self.prepare_internal(insert_sql)?;
4196                            *sub = SyncRowStep::Upsert {
4197                                stmt: Box::new(stmt),
4198                            };
4199                        }
4200                        SyncRowStep::Upsert { stmt } => {
4201                            crate::return_if_io!(stmt.run_with_row_callback_nonblock(|_| Ok(())));
4202                            if let Some(mv_store) = self.db.get_mv_store().as_ref() {
4203                                let watermark = rows[*idx].1;
4204                                let first_unsafe = watermark.checked_add(1).unwrap_or(watermark);
4205                                mv_store.set_sequence_watermark(
4206                                    &autoincrement_sequence_name(&rows[*idx].0),
4207                                    first_unsafe,
4208                                );
4209                            }
4210                            *idx += 1;
4211                            *sub = SyncRowStep::Start;
4212                        }
4213                    }
4214                }
4215            }
4216        }
4217    }
4218
4219    /// Create a `TempDir` honoring `TURSO_TMPDIR` and `SQLITE_TMPDIR`,
4220    /// falling back to the OS default (`env::temp_dir()`).
4221    ///
4222    /// `&self` is reserved for a future per-connection
4223    /// `temp_store_directory` setting (e.g. `PRAGMA temp_store_directory`)
4224    /// so call sites don't need to change when that lands.
4225    #[cfg(not(target_family = "wasm"))]
4226    pub(crate) fn create_tempdir(&self) -> Result<TempDir> {
4227        let res = if let Some(d) = std::env::var_os("TURSO_TMPDIR") {
4228            tempfile::tempdir_in(d)
4229        } else if let Some(d) = std::env::var_os("SQLITE_TMPDIR") {
4230            tempfile::tempdir_in(d)
4231        } else {
4232            tempfile::tempdir()
4233        };
4234        res.map_err(|e| io_error(e, "tempdir"))
4235    }
4236
4237    pub fn get_data_sync_retry(&self) -> bool {
4238        self.data_sync_retry
4239            .load(crate::sync::atomic::Ordering::SeqCst)
4240    }
4241
4242    pub fn set_data_sync_retry(&self, value: bool) {
4243        self.data_sync_retry
4244            .store(value, crate::sync::atomic::Ordering::SeqCst);
4245        self.bump_prepare_context_generation();
4246    }
4247
4248    /// Get the sync type setting.
4249    pub fn get_sync_type(&self) -> crate::io::FileSyncType {
4250        self.pager.load().get_sync_type()
4251    }
4252
4253    /// Set the sync type (for PRAGMA fullfsync).
4254    pub fn set_sync_type(&self, value: crate::io::FileSyncType) {
4255        self.pager.load().set_sync_type(value);
4256    }
4257
4258    /// Creates a HashSet of modules that have been loaded
4259    pub fn get_syms_vtab_mods(&self) -> HashSet<String> {
4260        self.syms.read().vtab_modules.keys().cloned().collect()
4261    }
4262
4263    /// Returns external (extension) functions: (name, is_aggregate, argc, deterministic)
4264    pub fn get_syms_functions(&self) -> Vec<(String, bool, i32, bool)> {
4265        self.syms
4266            .read()
4267            .functions
4268            .values()
4269            .map(|f| {
4270                let is_agg = f.func.is_aggregate();
4271                let argc = match &f.func {
4272                    function::ExtFunc::Aggregate { argc, .. } => *argc,
4273                    function::ExtFunc::Scalar { argc, .. } => *argc,
4274                };
4275                (
4276                    f.name.clone(),
4277                    is_agg,
4278                    argc,
4279                    function::Deterministic::is_deterministic(f.as_ref()),
4280                )
4281            })
4282            .collect()
4283    }
4284
4285    pub fn register_external_collation(
4286        &self,
4287        name: String,
4288        context: usize,
4289        callback: crate::ContextCollationFunction,
4290        context_destructor: Option<crate::ContextDestructor>,
4291    ) {
4292        let collation = CollationSeq::custom(&name);
4293        let normalized_name = crate::util::normalize_ident(&name);
4294        self.syms.write().collations.insert(
4295            collation.id(),
4296            Arc::new(function::ExternalCollation::new(
4297                normalized_name,
4298                context,
4299                callback,
4300                context_destructor,
4301            )),
4302        );
4303        self.bump_prepare_context_generation();
4304    }
4305
4306    pub fn unregister_external_collation(&self, name: &str) {
4307        if let Some(collation) = CollationSeq::known_custom(name) {
4308            if self
4309                .syms
4310                .write()
4311                .collations
4312                .remove(&collation.id())
4313                .is_some()
4314            {
4315                self.bump_prepare_context_generation();
4316            }
4317        }
4318    }
4319
4320    pub(crate) fn get_external_collation(
4321        &self,
4322        collation: CollationSeq,
4323    ) -> Result<Arc<function::ExternalCollation>> {
4324        self.syms
4325            .read()
4326            .collations
4327            .get(&collation.id())
4328            .cloned()
4329            .ok_or_else(|| {
4330                LimboError::ParseError(format!("no such collation sequence: {}", collation.name()))
4331            })
4332    }
4333
4334    pub(crate) fn custom_collation_compare(
4335        external: &function::ExternalCollation,
4336        left: &str,
4337        right: &str,
4338    ) -> CmpOrdering {
4339        let result = unsafe {
4340            (external.callback)(
4341                external.context,
4342                left.as_ptr(),
4343                left.len(),
4344                right.as_ptr(),
4345                right.len(),
4346            )
4347        };
4348        result.cmp(&0)
4349    }
4350
4351    pub(crate) fn external_collation_comparator(
4352        external: Arc<function::ExternalCollation>,
4353    ) -> crate::vdbe::sorter::SortComparator {
4354        Arc::new(move |left, right| {
4355            Ok(match (left, right) {
4356                (crate::ValueRef::Text(left), crate::ValueRef::Text(right)) => {
4357                    Self::custom_collation_compare(&external, left.as_str(), right.as_str())
4358                }
4359                _ => left.partial_cmp(right).unwrap_or(CmpOrdering::Equal),
4360            })
4361        })
4362    }
4363
4364    pub(crate) fn make_collation_comparator(
4365        &self,
4366        collation: CollationSeq,
4367    ) -> Result<crate::vdbe::sorter::SortComparator> {
4368        let external = self.get_external_collation(collation)?;
4369        Ok(Self::external_collation_comparator(external))
4370    }
4371
4372    pub(crate) fn compare_external_collation(
4373        &self,
4374        collation: CollationSeq,
4375        left: &str,
4376        right: &str,
4377    ) -> Result<CmpOrdering> {
4378        let external = self.get_external_collation(collation)?;
4379        Ok(Self::custom_collation_compare(&external, left, right))
4380    }
4381
4382    pub(crate) fn database_ptr(&self) -> usize {
4383        Arc::as_ptr(&self.db) as usize
4384    }
4385
4386    pub fn set_encryption_key(&self, key: EncryptionKey) -> Result<()> {
4387        tracing::trace!("setting encryption key for connection");
4388        self.ensure_can_change_encryption_settings()?;
4389        *self.encryption_key.write() = Some(key);
4390        self.bump_prepare_context_generation();
4391        self.set_encryption_context()
4392    }
4393
4394    pub fn set_encryption_cipher(&self, cipher_mode: CipherMode) -> Result<()> {
4395        tracing::trace!("setting encryption cipher for connection");
4396        self.ensure_can_change_encryption_settings()?;
4397        self.encryption_cipher_mode.set(cipher_mode);
4398        self.bump_prepare_context_generation();
4399        self.set_encryption_context()
4400    }
4401
4402    pub fn set_reserved_bytes(&self, reserved_bytes: u8) -> Result<()> {
4403        let pager = self.pager.load();
4404        pager.set_reserved_space_bytes(reserved_bytes);
4405        Ok(())
4406    }
4407
4408    /// Get the reserved bytes value from the pager cache.
4409    /// Returns None if not yet set (database not initialized).
4410    pub fn get_reserved_bytes(&self) -> Option<u8> {
4411        let pager = self.pager.load();
4412        pager.get_reserved_space()
4413    }
4414
4415    pub fn get_encryption_cipher_mode(&self) -> Option<CipherMode> {
4416        match self.encryption_cipher_mode.get() {
4417            CipherMode::None => None,
4418            mode => Some(mode),
4419        }
4420    }
4421
4422    fn ensure_can_change_encryption_settings(&self) -> Result<()> {
4423        let pager = self.pager.load();
4424        if pager.is_encryption_ctx_set() {
4425            return Err(LimboError::InvalidArgument(
4426                "cannot reset encryption attributes if already set in the session".to_string(),
4427            ));
4428        }
4429        if self.db.get_mv_store().is_some() {
4430            return Err(LimboError::InvalidArgument(
4431                "cannot enable encryption after MVCC is active; configure encryption before PRAGMA journal_mode='mvcc'"
4432                    .to_string(),
4433            ));
4434        }
4435        Ok(())
4436    }
4437
4438    // if both key and cipher are set, set encryption context on pager
4439    fn set_encryption_context(&self) -> Result<()> {
4440        let key_guard = self.encryption_key.read();
4441        let Some(key) = key_guard.as_ref() else {
4442            return Ok(());
4443        };
4444        let cipher_mode = self.get_encryption_cipher_mode();
4445        let Some(cipher_mode) = cipher_mode else {
4446            return Ok(());
4447        };
4448        tracing::trace!("setting encryption ctx for connection");
4449        let pager = self.pager.load();
4450        pager.set_encryption_context(cipher_mode, key)
4451    }
4452
4453    /// Sets a custom busy handler callback.
4454    pub fn set_busy_handler(&self, handler: Option<BusyHandlerCallback>) {
4455        *self.busy_handler.write() = match handler {
4456            Some(callback) => BusyHandler::Custom { callback },
4457            None => BusyHandler::None,
4458        };
4459        self.bump_prepare_context_generation();
4460    }
4461
4462    /// Sets maximum total accumulated timeout. If the duration is Zero, we unset the busy handler.
4463    pub fn set_busy_timeout(&self, duration: Duration) {
4464        *self.busy_handler.write() = if duration.is_zero() {
4465            BusyHandler::None
4466        } else {
4467            BusyHandler::Timeout(duration)
4468        };
4469        self.bump_prepare_context_generation();
4470    }
4471
4472    /// Get the busy timeout duration.
4473    pub fn get_busy_timeout(&self) -> Duration {
4474        match &*self.busy_handler.read() {
4475            BusyHandler::Timeout(d) => *d,
4476            _ => Duration::ZERO,
4477        }
4478    }
4479
4480    /// Sets the maximum duration a statement is allowed to run.
4481    /// `Duration::ZERO` disables query timeout.
4482    pub fn set_query_timeout(&self, duration: Duration) {
4483        let millis = duration.as_millis().min(u128::from(u64::MAX)) as u64;
4484        self.query_timeout_ms.store(millis, Ordering::SeqCst);
4485    }
4486
4487    /// Get the query timeout duration.
4488    pub fn get_query_timeout(&self) -> Duration {
4489        Duration::from_millis(self.query_timeout_ms.load(Ordering::SeqCst))
4490    }
4491
4492    /// Get a reference to the busy handler.
4493    pub fn get_busy_handler(&self) -> crate::sync::RwLockReadGuard<'_, BusyHandler> {
4494        self.busy_handler.read()
4495    }
4496
4497    /// Sets a progress handler invoked approximately every `ops` VM steps.
4498    /// Passing `ops == 0` or `None` disables the progress handler.
4499    pub fn set_progress_handler(&self, ops: u64, handler: Option<ProgressHandlerCallback>) {
4500        self.progress_handler.set(ops, handler);
4501    }
4502
4503    /// Returns true when the step-based progress handler requests interruption.
4504    pub fn should_interrupt_for_progress(&self, vm_steps: u64) -> bool {
4505        self.progress_handler.should_interrupt(vm_steps)
4506    }
4507
4508    /// Request interruption of currently running root statements on this connection.
4509    /// If no root statement is active, the request is ignored to match SQLite semantics.
4510    pub fn interrupt(&self) {
4511        if self.n_active_root_statements.load(Ordering::SeqCst) > 0 {
4512            self.interrupt_requested.store(true, Ordering::SeqCst);
4513        }
4514    }
4515
4516    /// Returns true if an interrupt is currently pending for this connection.
4517    pub fn is_interrupted(&self) -> bool {
4518        self.interrupt_requested.load(Ordering::SeqCst)
4519    }
4520
4521    /// Clear the connection interrupt once no root statements remain active.
4522    pub(crate) fn clear_interrupt_if_idle(&self) {
4523        if self.n_active_root_statements.load(Ordering::SeqCst) == 0 {
4524            self.interrupt_requested.store(false, Ordering::SeqCst);
4525        }
4526    }
4527
4528    pub(crate) fn set_tx_state(&self, state: TransactionState) {
4529        self.transaction_state.set(state);
4530    }
4531
4532    pub(crate) fn get_tx_state(&self) -> TransactionState {
4533        self.transaction_state.get()
4534    }
4535
4536    /// Returns true if the connection is currently in a write transaction.
4537    /// Used by index methods to determine if it's safe to flush writes.
4538    pub fn is_in_write_tx(&self) -> bool {
4539        matches!(self.get_tx_state(), TransactionState::Write { .. })
4540    }
4541
4542    pub(crate) fn get_mv_tx_id(&self) -> Option<u64> {
4543        self.mv_tx.read().map(|(tx_id, _)| tx_id)
4544    }
4545
4546    pub(crate) fn get_mv_tx(&self) -> Option<(u64, TransactionMode)> {
4547        *self.mv_tx.read()
4548    }
4549
4550    #[inline(always)]
4551    pub(crate) fn set_mv_tx(&self, tx_id_and_mode: Option<(u64, TransactionMode)>) {
4552        tracing::debug!("set_mv_tx: {:?}", tx_id_and_mode);
4553        *self.mv_tx.write() = tx_id_and_mode;
4554    }
4555
4556    /// Get MVCC transaction ID for a specific database.
4557    /// Uses fast path for main DB, O(1) HashMap lookup for attached DBs.
4558    pub(crate) fn get_mv_tx_id_for_db(&self, db: usize) -> Option<u64> {
4559        if db == crate::MAIN_DB_ID {
4560            self.get_mv_tx_id()
4561        } else {
4562            self.attached_mv_txs
4563                .read()
4564                .get(&db)
4565                .map(|(tx_id, _)| *tx_id)
4566        }
4567    }
4568
4569    /// Get MVCC transaction ID and mode for a specific database.
4570    pub(crate) fn get_mv_tx_for_db(&self, db: usize) -> Option<(u64, TransactionMode)> {
4571        if db == crate::MAIN_DB_ID {
4572            self.get_mv_tx()
4573        } else {
4574            self.attached_mv_txs.read().get(&db).copied()
4575        }
4576    }
4577
4578    /// Set MVCC transaction for a specific database.
4579    pub(crate) fn set_mv_tx_for_db(&self, db: usize, val: Option<(u64, TransactionMode)>) {
4580        if db == crate::MAIN_DB_ID {
4581            self.set_mv_tx(val);
4582        } else {
4583            let mut txs = self.attached_mv_txs.write();
4584            match val {
4585                Some(v) => {
4586                    txs.insert(db, v);
4587                }
4588                None => {
4589                    txs.remove(&db);
4590                }
4591            }
4592        }
4593    }
4594
4595    /// Rollback MVCC transactions on all attached databases and clear the
4596    /// attached transaction list.  When `clear_schemas` is true the
4597    /// connection-local schema cache for each attached DB is also removed so
4598    /// that post-rollback queries see the committed schema.
4599    ///
4600    /// This is the single source of truth for attached-MVCC rollback logic —
4601    /// callers in `close()`, `rollback_current_txn()`, and `op_auto_commit`
4602    /// should all delegate here.
4603    pub(crate) fn rollback_attached_mvcc_txs(&self, clear_schemas: bool) {
4604        let txs: HashMap<usize, _> = self.attached_mv_txs.read().clone();
4605        let mut cleared_any_schema = false;
4606        for (&db_id, &(tx_id, _mode)) in &txs {
4607            if let Some(attached_mv_store) = self.mv_store_for_db(db_id) {
4608                let attached_pager = self
4609                    .get_pager_from_database_index(&db_id)
4610                    .expect("attached MVCC transaction should always have a pager");
4611                if attached_mv_store.is_tx_rollbackable(tx_id) {
4612                    attached_mv_store.rollback_tx(tx_id, attached_pager.clone(), self, db_id);
4613                } else {
4614                    self.set_mv_tx_for_db(db_id, None);
4615                }
4616                if clear_schemas {
4617                    self.database_schemas().write().remove(&db_id);
4618                    cleared_any_schema = true;
4619                }
4620                attached_pager.end_read_tx();
4621            }
4622        }
4623        self.attached_mv_txs.write().clear();
4624        if cleared_any_schema {
4625            self.bump_prepare_context_generation();
4626        }
4627    }
4628
4629    /// Rollback WAL-mode transactions on all attached databases and discard
4630    /// their connection-local schema caches.  MVCC-enabled attached databases
4631    /// are skipped — those are handled by `rollback_attached_mvcc_txs`.
4632    pub(crate) fn rollback_attached_wal_txns(&self) {
4633        self.with_all_attached_pagers_with_index(|pagers| {
4634            // Record indices of WAL-mode entries so we can batch the schema
4635            // removal under a single write lock and avoid calling
4636            // `mv_store_for_db` more than once per entry.
4637            let mut wal_indices: SmallVec<[usize; 4]> = SmallVec::new();
4638            for (i, (db_id, _)) in pagers.iter().enumerate() {
4639                if self.mv_store_for_db(*db_id).is_none() {
4640                    wal_indices.push(i);
4641                }
4642            }
4643            if wal_indices.is_empty() {
4644                return;
4645            }
4646            {
4647                let mut schemas = self.database_schemas().write();
4648                for &i in &wal_indices {
4649                    schemas.remove(&pagers[i].0);
4650                }
4651            }
4652            self.bump_prepare_context_generation();
4653            for &i in &wal_indices {
4654                pagers[i].1.rollback_attached();
4655            }
4656        });
4657    }
4658
4659    pub(crate) fn with_named_savepoints<F, T>(&self, f: F) -> T
4660    where
4661        F: FnOnce(&[NamedSavepointFrame]) -> T,
4662    {
4663        let savepoints = self.named_savepoints.read();
4664        f(&savepoints)
4665    }
4666
4667    pub(crate) fn push_named_savepoint(&self, frame: NamedSavepointFrame) {
4668        self.named_savepoints.write().push(frame);
4669    }
4670
4671    /// Snapshot the in-memory schemas (main, temp, attached) for a
4672    /// savepoint frame so ROLLBACK TO can restore them without re-
4673    /// reading sqlite_schema from disk. Disk reparse from inside the
4674    /// vdbe ROLLBACK TO opcode would block on cursor I/O and violate
4675    /// the vdbe async contract.
4676    pub(crate) fn with_savepoint_schema_snapshot<F, T>(&self, f: F) -> T
4677    where
4678        F: FnOnce(Arc<Schema>, Option<Arc<Schema>>, HashMap<usize, Arc<Schema>>) -> T,
4679    {
4680        let main_schema_snapshot = self.schema.read().clone();
4681        let temp_schema_snapshot = self
4682            .temp
4683            .database
4684            .read()
4685            .as_ref()
4686            .map(|temp_db| temp_db.db.schema.lock().clone());
4687        let staged_schema_snapshot = self.database_schemas.read().clone();
4688        f(
4689            main_schema_snapshot,
4690            temp_schema_snapshot,
4691            staged_schema_snapshot,
4692        )
4693    }
4694
4695    pub(crate) fn release_named_savepoint_frame(&self, name: &str) -> SavepointResult {
4696        let mut savepoints = self.named_savepoints.write();
4697        let Some(target_idx) = savepoints
4698            .iter()
4699            .rposition(|savepoint| savepoint.name == name)
4700        else {
4701            return SavepointResult::NotFound;
4702        };
4703        if savepoints[target_idx].starts_transaction && target_idx == 0 {
4704            return SavepointResult::Commit;
4705        }
4706        savepoints.truncate(target_idx);
4707        SavepointResult::Release
4708    }
4709
4710    pub(crate) fn rollback_named_savepoint_frame(&self, name: &str) -> Option<RollbackFrameInfo> {
4711        let mut savepoints = self.named_savepoints.write();
4712        let target_idx = savepoints
4713            .iter()
4714            .rposition(|savepoint| savepoint.name == name)?;
4715        let frame = &savepoints[target_idx];
4716        let info = RollbackFrameInfo {
4717            main_schema_snapshot: frame.main_schema_snapshot.clone(),
4718            temp_schema_snapshot: frame.temp_schema_snapshot.clone(),
4719            staged_schema_snapshot: frame.staged_schema_snapshot.clone(),
4720        };
4721        // ROLLBACK TO keeps the target savepoint itself on the stack;
4722        // only nested savepoints above it are discarded.
4723        savepoints.truncate(target_idx + 1);
4724        Some(info)
4725    }
4726
4727    pub(crate) fn clear_named_savepoints(&self) {
4728        self.named_savepoints.write().clear();
4729    }
4730
4731    /// Roll back the current main-db transaction state and any attached-db
4732    /// transaction state on this connection.
4733    pub(crate) fn rollback_current_txn_state(
4734        &self,
4735        pager: &Arc<Pager>,
4736        clear_attached_schemas: bool,
4737    ) {
4738        if let Some(mv_store) = self.mv_store().as_ref() {
4739            if let Some(tx_id) = self.get_mv_tx_id() {
4740                self.auto_commit.store(true, Ordering::SeqCst);
4741                if mv_store.is_tx_rollbackable(tx_id) {
4742                    mv_store.rollback_tx(tx_id, pager.clone(), self, crate::MAIN_DB_ID);
4743                } else {
4744                    self.set_mv_tx(None);
4745                }
4746            }
4747            pager.end_read_tx();
4748            self.rollback_attached_mvcc_txs(clear_attached_schemas);
4749        } else {
4750            pager.rollback_tx(self);
4751            self.auto_commit.store(true, Ordering::SeqCst);
4752        }
4753        self.rollback_attached_wal_txns();
4754        self.set_tx_state(TransactionState::None);
4755        self.clear_tx_poison();
4756    }
4757
4758    /// Roll back transaction state for helpers that start a manual `BEGIN`
4759    /// outside the normal Transaction opcode path.
4760    ///
4761    /// Unlike `rollback_current_txn_state`, this tolerates the attached-only
4762    /// case where the connection flipped `auto_commit` off but never opened a
4763    /// main-db read transaction.
4764    pub(crate) fn rollback_manual_txn_cleanup(
4765        &self,
4766        pager: &Arc<Pager>,
4767        clear_attached_schemas: bool,
4768    ) {
4769        let main_has_implicit_state = self.get_tx_state() != TransactionState::None
4770            || self.get_mv_tx().is_some()
4771            || pager.holds_read_lock()
4772            || pager.holds_write_lock();
4773
4774        if main_has_implicit_state {
4775            self.rollback_current_txn_state(pager, clear_attached_schemas);
4776        } else {
4777            if self.next_attached_mv_tx().is_some() {
4778                self.rollback_attached_mvcc_txs(clear_attached_schemas);
4779            }
4780            self.rollback_attached_wal_txns();
4781            self.set_tx_state(TransactionState::None);
4782            self.auto_commit.store(true, Ordering::SeqCst);
4783        }
4784
4785        self.rollback_temp_schema();
4786        self.clear_tx_poison();
4787        self.set_cdc_transaction_id(-1);
4788        self.clear_named_savepoints();
4789        self.clear_deferred_foreign_key_violations();
4790    }
4791
4792    /// Iterate over all attached MVCC transactions, calling `f(db_id, tx_id)` for each.
4793    pub(crate) fn for_each_attached_mv_tx(&self, mut f: impl FnMut(usize, u64)) {
4794        let txs = self.attached_mv_txs.read();
4795        for (&db_id, &(tx_id, _)) in txs.iter() {
4796            f(db_id, tx_id);
4797        }
4798    }
4799
4800    /// Get the next attached MVCC transaction.
4801    /// Returns an arbitrary entry from `attached_mv_txs`, or `None` if empty.
4802    pub(crate) fn next_attached_mv_tx(&self) -> Option<(usize, u64, TransactionMode)> {
4803        self.attached_mv_txs
4804            .read()
4805            .iter()
4806            .next()
4807            .map(|(&db_id, &(tx_id, mode))| (db_id, tx_id, mode))
4808    }
4809
4810    /// Get the MvStore for a specific database.
4811    /// Returns None for databases without MVCC or for bootstrap connections.
4812    pub(crate) fn mv_store_for_db(&self, db: usize) -> Option<Arc<MvStore>> {
4813        if self.is_mvcc_bootstrap_connection() {
4814            return None;
4815        }
4816        match db {
4817            crate::MAIN_DB_ID => self.db.get_mv_store().as_ref().cloned(),
4818            crate::TEMP_DB_ID => None,
4819            _ => {
4820                let catalog = self.attached_databases.read();
4821                catalog
4822                    .index_to_data
4823                    .get(&db)
4824                    .and_then(|(db, _)| db.get_mv_store().as_ref().cloned())
4825            }
4826        }
4827    }
4828
4829    pub(crate) fn set_mvcc_checkpoint_threshold(&self, threshold: i64) -> Result<()> {
4830        match self.db.get_mv_store().as_ref() {
4831            Some(mv_store) => {
4832                mv_store.set_checkpoint_threshold(threshold);
4833                self.bump_prepare_context_generation();
4834                Ok(())
4835            }
4836            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4837        }
4838    }
4839
4840    pub(crate) fn mvcc_checkpoint_threshold(&self) -> Result<i64> {
4841        match self.db.get_mv_store().as_ref() {
4842            Some(mv_store) => Ok(mv_store.checkpoint_threshold()),
4843            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4844        }
4845    }
4846
4847    pub(crate) fn set_mvcc_gc_threshold(&self, threshold: i64) -> Result<()> {
4848        match self.db.get_mv_store().as_ref() {
4849            Some(mv_store) => {
4850                mv_store.set_gc_threshold(threshold);
4851                self.bump_prepare_context_generation();
4852                Ok(())
4853            }
4854            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4855        }
4856    }
4857
4858    pub(crate) fn mvcc_gc_threshold(&self) -> Result<i64> {
4859        match self.db.get_mv_store().as_ref() {
4860            Some(mv_store) => Ok(mv_store.gc_threshold()),
4861            None => Err(LimboError::InternalError("MVCC not enabled".into())),
4862        }
4863    }
4864
4865    pub(crate) fn mvcc_tx_should_abort(&self) -> bool {
4866        match (self.db.get_mv_store().clone(), self.get_mv_tx_id()) {
4867            (Some(mv_store), Some(tx_id)) => mv_store.tx_should_abort(tx_id),
4868            _ => false,
4869        }
4870    }
4871}
4872
4873pub type Row = vdbe::Row;
4874
4875pub type StepResult = vdbe::StepResult;
4876
4877#[derive(Default)]
4878pub struct SymbolTable {
4879    pub functions: HashMap<String, Arc<function::ExternalFunc>>,
4880    pub collations: HashMap<u32, Arc<function::ExternalCollation>>,
4881    pub vtabs: HashMap<String, Arc<VirtualTable>>,
4882    pub vtab_modules: HashMap<String, Arc<crate::ext::VTabImpl>>,
4883    pub index_methods: HashMap<String, Arc<dyn IndexMethod>>,
4884}
4885
4886impl std::fmt::Debug for SymbolTable {
4887    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4888        f.debug_struct("SymbolTable")
4889            .field("functions", &self.functions)
4890            .field("collations", &self.collations)
4891            .finish()
4892    }
4893}
4894
4895fn is_shared_library(path: &std::path::Path) -> bool {
4896    path.extension()
4897        .is_some_and(|ext| ext == "so" || ext == "dylib" || ext == "dll")
4898}
4899
4900pub fn resolve_ext_path(extpath: &str) -> Result<std::path::PathBuf> {
4901    let path = std::path::Path::new(extpath);
4902    if !path.exists() {
4903        if is_shared_library(path) {
4904            return Err(LimboError::ExtensionError(format!(
4905                "Extension file not found: {extpath}"
4906            )));
4907        };
4908        let maybe = path.with_extension(std::env::consts::DLL_EXTENSION);
4909        maybe.exists().then_some(maybe).ok_or_else(|| {
4910            LimboError::ExtensionError(format!("Extension file not found: {extpath}"))
4911        })
4912    } else {
4913        Ok(path.to_path_buf())
4914    }
4915}
4916
4917impl SymbolTable {
4918    pub fn new() -> Self {
4919        Self {
4920            functions: HashMap::default(),
4921            collations: HashMap::default(),
4922            vtabs: HashMap::default(),
4923            vtab_modules: HashMap::default(),
4924            index_methods: HashMap::default(),
4925        }
4926    }
4927    pub fn resolve_function(
4928        &self,
4929        name: &str,
4930        arg_count: usize,
4931    ) -> Option<Arc<function::ExternalFunc>> {
4932        self.functions
4933            .get(name)
4934            .cloned()
4935            .or_else(|| {
4936                self.functions
4937                    .get(&crate::util::normalize_ident(name))
4938                    .cloned()
4939            })
4940            .filter(|func| func.func.matches_arg_count(arg_count))
4941    }
4942
4943    pub fn resolve_collation(&self, name: &str) -> Option<CollationSeq> {
4944        let collation = CollationSeq::known_custom(name)?;
4945        self.collations
4946            .contains_key(&collation.id())
4947            .then_some(collation)
4948    }
4949
4950    pub fn extend(&mut self, other: &SymbolTable) {
4951        for (name, func) in &other.functions {
4952            self.functions.insert(name.clone(), func.clone());
4953        }
4954        for (id, collation) in &other.collations {
4955            self.collations.insert(*id, collation.clone());
4956        }
4957        for (name, vtab) in &other.vtabs {
4958            self.vtabs.insert(name.clone(), vtab.clone());
4959        }
4960        for (name, module) in &other.vtab_modules {
4961            self.vtab_modules.insert(name.clone(), module.clone());
4962        }
4963        for (name, module) in &other.index_methods {
4964            self.index_methods.insert(name.clone(), module.clone());
4965        }
4966    }
4967}
4968
4969#[cfg(all(clt_turso_tests, clt_turso_feature = "fs"))]
4970mod tests {
4971    use super::*;
4972    use tempfile::TempDir;
4973
4974    fn open_connection_with_opts(path: &std::path::Path, opts: DatabaseOpts) -> Arc<Connection> {
4975        let io: Arc<dyn IO> = Arc::new(crate::PlatformIO::new().unwrap());
4976        let db = Database::open_file_with_flags(
4977            io,
4978            path.to_str().unwrap(),
4979            OpenFlags::default(),
4980            opts,
4981            None,
4982        )
4983        .unwrap();
4984        db.connect().unwrap()
4985    }
4986
4987    fn open_connection(path: &std::path::Path) -> Arc<Connection> {
4988        open_connection_with_opts(path, DatabaseOpts::new())
4989    }
4990
4991    fn drive_attach(conn: &Arc<Connection>, path: &str, alias: &str) -> Result<()> {
4992        let mut state = AttachDatabaseState::default();
4993        loop {
4994            match conn.attach_database(path, alias, &mut state)? {
4995                IOResult::Done(()) => return Ok(()),
4996                IOResult::IO(io) => io.wait(conn.db.io.as_ref())?,
4997            }
4998        }
4999    }
5000
5001    fn drive_attach_with_config(
5002        conn: &Arc<Connection>,
5003        path: &str,
5004        alias: &str,
5005        reserved_space: Option<u8>,
5006    ) -> Result<()> {
5007        let mut state = AttachDatabaseState::default();
5008        loop {
5009            match conn.attach_database_with_config(path, alias, reserved_space, &mut state)? {
5010                IOResult::Done(()) => return Ok(()),
5011                IOResult::IO(io) => io.wait(conn.db.io.as_ref())?,
5012            }
5013        }
5014    }
5015
5016    fn query_single_i64(conn: &Arc<Connection>, sql: &str) -> i64 {
5017        let mut stmt = conn.prepare(sql).unwrap();
5018        match stmt.step().unwrap() {
5019            StepResult::Row => stmt.row().unwrap().get::<i64>(0).unwrap(),
5020            other => panic!("expected a row, got {other:?}"),
5021        }
5022    }
5023
5024    fn text_value(value: &Value) -> &str {
5025        match value {
5026            Value::Text(text) => text.as_str(),
5027            other => panic!("expected text value, got {other:?}"),
5028        }
5029    }
5030
5031    // given a attached 'alias', return the Database and Pager for that attached database
5032    fn attached_entry(conn: &Connection, alias: &str) -> (Arc<Database>, Arc<Pager>) {
5033        let catalog = conn.attached_databases.read();
5034        let index = *catalog.name_to_index.get(alias).unwrap();
5035        catalog.index_to_data.get(&index).unwrap().clone()
5036    }
5037
5038    #[test]
5039    fn test_named_memory_databases_on_same_io_are_distinct() {
5040        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5041        let draft_db = Database::open_file(io.clone(), ":memory:sync-draft").unwrap();
5042        let synced_db = Database::open_file(io, ":memory:sync-synced").unwrap();
5043        assert!(!Arc::ptr_eq(&draft_db, &synced_db));
5044
5045        let draft = draft_db.connect().unwrap();
5046        let synced = synced_db.connect().unwrap();
5047
5048        for conn in [&draft, &synced] {
5049            assert_eq!(conn.get_database_canonical_path(), "");
5050            assert_eq!(
5051                conn.list_all_databases(),
5052                vec![(MAIN_DB_ID, "main".to_string(), String::new())]
5053            );
5054        }
5055
5056        draft
5057            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(11)")
5058            .unwrap();
5059        synced
5060            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(22)")
5061            .unwrap();
5062
5063        assert_eq!(query_single_i64(&draft, "SELECT x FROM t"), 11);
5064        assert_eq!(query_single_i64(&synced, "SELECT x FROM t"), 22);
5065    }
5066
5067    #[test]
5068    fn test_named_memory_database_reopened_on_same_io_sees_same_rows() {
5069        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5070
5071        let first_db = Database::open_file(io.clone(), ":memory:reopen").unwrap();
5072        let first = first_db.connect().unwrap();
5073        first
5074            .execute("CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(99)")
5075            .unwrap();
5076
5077        let second_db = Database::open_file(io, ":memory:reopen").unwrap();
5078        let second = second_db.connect().unwrap();
5079        assert_eq!(query_single_i64(&second, "SELECT x FROM t"), 99);
5080    }
5081
5082    #[test]
5083    fn test_attach_named_memory_database_reports_empty_path() {
5084        let temp_dir = TempDir::new().unwrap();
5085        let main_path = temp_dir.path().join("main.db");
5086        let conn = open_connection_with_opts(&main_path, DatabaseOpts::new().with_attach(true));
5087
5088        conn.execute("ATTACH ':memory:aux' AS aux").unwrap();
5089        conn.execute("CREATE TABLE aux.t(x INTEGER); INSERT INTO aux.t VALUES(5)")
5090            .unwrap();
5091
5092        assert_eq!(query_single_i64(&conn, "SELECT x FROM aux.t"), 5);
5093        let database_list = conn.pragma_query("database_list").unwrap();
5094        let aux = database_list
5095            .iter()
5096            .find(|row| text_value(&row[1]) == "aux")
5097            .expect("attached aux database must be listed");
5098        assert_eq!(text_value(&aux[2]), "");
5099    }
5100
5101    #[test]
5102    fn test_named_memory_parent_can_attach_real_file_database() {
5103        let temp_dir = TempDir::new().unwrap();
5104        let aux_path = temp_dir.path().join("aux.db");
5105        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
5106        let db = Database::open_file_with_flags(
5107            io,
5108            ":memory:named-main",
5109            OpenFlags::default(),
5110            DatabaseOpts::new().with_attach(true),
5111            None,
5112        )
5113        .unwrap();
5114        let conn = db.connect().unwrap();
5115
5116        conn.execute(format!("ATTACH '{}' AS aux", aux_path.to_str().unwrap()))
5117            .unwrap();
5118        conn.execute("CREATE TABLE aux.t(x INTEGER); INSERT INTO aux.t VALUES(7)")
5119            .unwrap();
5120        conn.execute("DETACH aux").unwrap();
5121
5122        let reopened = open_connection(&aux_path);
5123        assert_eq!(query_single_i64(&reopened, "SELECT x FROM t"), 7);
5124    }
5125
5126    #[test]
5127    fn test_attach_database_with_config_overrides_reserved_space_before_initialization() {
5128        let temp_dir = TempDir::new().unwrap();
5129        let main_path = temp_dir.path().join("main.db");
5130        let aux_path = temp_dir.path().join("aux.db");
5131        let conn = open_connection(&main_path);
5132
5133        drive_attach_with_config(&conn, aux_path.to_str().unwrap(), "aux", Some(48)).unwrap();
5134
5135        let (attached_db, pager) = attached_entry(&conn, "aux");
5136        assert!(!attached_db.initialized());
5137        assert!(!pager.db_initialized());
5138        assert_eq!(pager.get_reserved_space(), Some(48));
5139    }
5140
5141    #[cfg(clt_turso_feature = "checksum")]
5142    #[test]
5143    fn test_attach_database_with_config_rejects_reserved_space_below_minimum() {
5144        let temp_dir = TempDir::new().unwrap();
5145        let main_path = temp_dir.path().join("main.db");
5146        let aux_path = temp_dir.path().join("aux.db");
5147        let conn = open_connection(&main_path);
5148
5149        let err = drive_attach_with_config(&conn, aux_path.to_str().unwrap(), "aux", Some(0))
5150            .unwrap_err()
5151            .to_string();
5152        assert_eq!(
5153            err,
5154            "Invalid argument supplied: cannot attach database 'aux': reserved space 0 is smaller than attached database minimum 8"
5155        );
5156    }
5157
5158    #[test]
5159    fn test_fresh_mvcc_attach_installs_wal_before_bootstrap() {
5160        // this is a test to check that mvcc db on attach with a fresh db, makes the
5161        // attached db also mvcc
5162        let temp_dir = TempDir::new().unwrap();
5163        let main_path = temp_dir.path().join("main.db");
5164        let aux_path = temp_dir.path().join("aux.db");
5165        let conn = open_connection(&main_path);
5166
5167        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5168        drive_attach(&conn, aux_path.to_str().unwrap(), "aux").unwrap();
5169
5170        let (attached_db, pager) = attached_entry(&conn, "aux");
5171        assert!(attached_db.get_mv_store().as_ref().is_some());
5172        assert!(pager.has_wal());
5173
5174        conn.execute("CREATE TABLE aux.t(x INTEGER)").unwrap();
5175        conn.execute("INSERT INTO aux.t VALUES(1)").unwrap();
5176        conn.execute("PRAGMA aux.wal_checkpoint(TRUNCATE)").unwrap();
5177    }
5178
5179    #[test]
5180    fn test_fresh_mvcc_attach_reuses_database_shared_wal() {
5181        let temp_dir = TempDir::new().unwrap();
5182        let main_path = temp_dir.path().join("main.db");
5183        let aux_path = temp_dir.path().join("aux.db");
5184        let conn = open_connection(&main_path);
5185
5186        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5187        drive_attach(&conn, aux_path.to_str().unwrap(), "aux").unwrap();
5188        conn.execute("CREATE TABLE aux.t(x INTEGER)").unwrap();
5189        conn.execute("INSERT INTO aux.t VALUES(1)").unwrap();
5190
5191        let (attached_db, pager) = attached_entry(&conn, "aux");
5192        let pager_shared_ptr = pager
5193            .wal_shared_ptr()
5194            .expect("fresh MVCC attach must expose WAL shared state in tests");
5195        let db_shared_ptr = Arc::as_ptr(&attached_db.shared_wal) as usize;
5196
5197        assert_eq!(pager_shared_ptr, db_shared_ptr);
5198    }
5199
5200    #[test]
5201    fn test_temp_tables_are_connection_local_and_shadow_main() {
5202        let temp_dir = TempDir::new().unwrap();
5203        let db_path = temp_dir.path().join("main.db");
5204        let conn1 = open_connection(&db_path);
5205
5206        conn1.execute("CREATE TABLE t(x INTEGER)").unwrap();
5207        conn1.execute("INSERT INTO main.t VALUES(1)").unwrap();
5208        let conn2 = open_connection(&db_path);
5209        conn1.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5210        conn1.execute("INSERT INTO temp.t VALUES(2)").unwrap();
5211
5212        assert_eq!(query_single_i64(&conn1, "SELECT x FROM t"), 2);
5213        assert_eq!(query_single_i64(&conn1, "SELECT x FROM main.t"), 1);
5214        assert_eq!(query_single_i64(&conn2, "SELECT x FROM t"), 1);
5215
5216        let err = conn2
5217            .prepare("SELECT x FROM temp.t")
5218            .unwrap_err()
5219            .to_string();
5220        assert!(
5221            err.contains("no such table"),
5222            "expected no such table error, got: {err}"
5223        );
5224    }
5225
5226    #[test]
5227    fn test_reprepare_after_temp_store_reset_does_not_panic() {
5228        let temp_dir = TempDir::new().unwrap();
5229        let db_path = temp_dir.path().join("main.db");
5230        let conn = open_connection(&db_path);
5231
5232        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5233        let mut stmt = conn.prepare("SELECT x FROM t").unwrap();
5234
5235        conn.execute("PRAGMA temp_store = MEMORY").unwrap();
5236
5237        let err = stmt.step().unwrap_err().to_string();
5238        assert!(
5239            err.contains("no such table"),
5240            "expected no such table after temp reset, got: {err}"
5241        );
5242    }
5243
5244    #[test]
5245    fn test_temp_trigger_abort_rolls_back_temp_writes_without_panicking() {
5246        let temp_dir = TempDir::new().unwrap();
5247        let db_path = temp_dir.path().join("main.db");
5248        let conn = open_connection(&db_path);
5249
5250        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5251        conn.execute("CREATE TEMP TABLE u(y INTEGER)").unwrap();
5252        conn.execute(
5253            "CREATE TRIGGER tr BEFORE INSERT ON temp.t BEGIN \
5254             INSERT INTO u VALUES (NEW.x); \
5255             SELECT RAISE(ABORT, 'boom'); \
5256             END;",
5257        )
5258        .unwrap();
5259
5260        let err = conn.execute("INSERT INTO temp.t VALUES(1)").unwrap_err();
5261        assert!(
5262            err.to_string().contains("boom"),
5263            "expected trigger abort error, got: {err}"
5264        );
5265        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.u"), 0);
5266        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.t"), 0);
5267    }
5268
5269    #[test]
5270    fn test_temp_trigger_abort_rolls_back_main_and_temp_writes() {
5271        let temp_dir = TempDir::new().unwrap();
5272        let db_path = temp_dir.path().join("main.db");
5273        let conn = open_connection(&db_path);
5274
5275        conn.execute("CREATE TABLE m(x INTEGER)").unwrap();
5276        conn.execute("CREATE TEMP TABLE t(x INTEGER)").unwrap();
5277        conn.execute("CREATE TEMP TABLE u(y INTEGER)").unwrap();
5278        conn.execute(
5279            "CREATE TRIGGER tr BEFORE INSERT ON temp.t BEGIN \
5280             INSERT INTO m VALUES (NEW.x); \
5281             INSERT INTO u VALUES (NEW.x); \
5282             SELECT RAISE(ABORT, 'boom'); \
5283             END;",
5284        )
5285        .unwrap();
5286
5287        let err = conn.execute("INSERT INTO temp.t VALUES(1)").unwrap_err();
5288        assert!(
5289            err.to_string().contains("boom"),
5290            "expected trigger abort error, got: {err}"
5291        );
5292        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM main.m"), 0);
5293        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.u"), 0);
5294        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM temp.t"), 0);
5295    }
5296
5297    #[test]
5298    fn test_distinct_triggers_with_same_name_in_different_schemas_can_fire_nested() {
5299        let temp_dir = TempDir::new().unwrap();
5300        let db_path = temp_dir.path().join("main.db");
5301        let conn = open_connection(&db_path);
5302
5303        conn.execute("CREATE TABLE src(x INTEGER)").unwrap();
5304        conn.execute("CREATE TABLE dst(y INTEGER)").unwrap();
5305        conn.execute("CREATE TABLE audit(z INTEGER)").unwrap();
5306        conn.execute(
5307            "CREATE TRIGGER shared_name AFTER INSERT ON dst BEGIN \
5308             INSERT INTO audit VALUES (NEW.y); \
5309             END;",
5310        )
5311        .unwrap();
5312        conn.execute(
5313            "CREATE TEMP TRIGGER shared_name AFTER INSERT ON main.src BEGIN \
5314             INSERT INTO dst VALUES (NEW.x); \
5315             END;",
5316        )
5317        .unwrap();
5318
5319        conn.execute("INSERT INTO src VALUES(7)").unwrap();
5320
5321        assert_eq!(query_single_i64(&conn, "SELECT COUNT(*) FROM main.dst"), 1);
5322        assert_eq!(query_single_i64(&conn, "SELECT SUM(z) FROM main.audit"), 7);
5323    }
5324
5325    /// A committed `setval(X, false)` stores an unconsumed sequence value.
5326    /// After sequence initialization reloads persisted state, the in-memory
5327    /// sequence must still represent that value as unconsumed, so the next
5328    /// `nextval()` returns `X` rather than advancing past it.
5329    /// Disk-only sequence design: setval(value, is_called=false) must be
5330    /// observable as the next nextval() result. Previously this exercised
5331    /// the in-memory-atomic reseeding path; that path no longer exists,
5332    /// but the user-visible contract still holds because every nextval
5333    /// reads the backing-table watermark and applies is_called semantics
5334    /// in op_sequence_compute_next.
5335    #[test]
5336    fn test_setval_uncalled_emits_stored_value_as_next() -> Result<()> {
5337        let temp_dir = TempDir::new().unwrap();
5338        let path = temp_dir.path().join("seq_init.db");
5339        let conn = open_connection_with_opts(&path, DatabaseOpts::new());
5340
5341        conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap();
5342        conn.execute("CREATE SEQUENCE s START 1 INCREMENT 3")?;
5343        conn.execute("SELECT setval('s', 13, 0)")?;
5344
5345        let next_val = query_single_i64(&conn, "SELECT nextval('s')");
5346        assert_eq!(
5347            next_val, 13,
5348            "setval(13, false) committed: next nextval must return 13"
5349        );
5350        Ok(())
5351    }
5352}