Skip to main content

clt_database/
lib.rs

1#![cfg_attr(
2    nightly,
3    feature(
4        allocator_api,
5        btreemap_alloc,
6        clone_from_ref,
7        min_specialization,
8        try_with_capacity,
9        trusted_len,
10        vec_push_within_capacity
11    )
12)]
13#![recursion_limit = "256"]
14// Keep upstream lint policy separate from the CLT application.
15#![allow(warnings, clippy::all)]
16
17/// CLT's required reader ownership, reseeding, and disk-scan fixes are present.
18/// Increment when CLT starts depending on an additional core correctness fix.
19pub const CLT_WAL_PATCH_LEVEL: u32 = 1;
20
21pub mod alloc;
22pub mod busy;
23#[cfg(clt_turso_feature = "cli_only")]
24pub mod dbpage;
25#[cfg(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench"))]
26pub mod functions;
27pub mod index_method;
28pub mod io;
29#[cfg(all(clt_turso_feature = "json", any(clt_turso_feature = "fuzz", clt_turso_feature = "bench")))]
30pub mod json;
31#[cfg(all(
32    clt_turso_tests,
33    clt_turso_feature = "fs",
34    host_shared_wal,
35    any(not(target_os = "windows"), clt_turso_feature = "experimental_win_iocp")
36))]
37mod multiprocess_tests;
38pub mod mvcc;
39#[cfg(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench"))]
40pub mod numeric;
41pub mod schema;
42pub mod skiplist;
43pub mod state_machine;
44pub mod storage;
45pub mod types;
46#[cfg(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench"))]
47pub mod vdbe;
48pub mod vector;
49
50#[cfg(clt_turso_feature = "cli_only")]
51pub(crate) mod btree_dump;
52pub(crate) mod sync;
53pub(crate) mod thread;
54
55mod assert;
56mod connection;
57mod dialect;
58mod error;
59mod ext;
60mod fast_lock;
61mod function;
62#[cfg(not(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench")))]
63mod functions;
64mod incremental;
65mod info;
66#[cfg(all(clt_turso_feature = "json", not(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench"))))]
67mod json;
68#[cfg(not(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench")))]
69mod numeric;
70mod parameters;
71#[cfg(clt_turso_feature = "percentile")]
72mod percentile;
73mod pragma;
74mod progress;
75mod pseudo;
76mod regexp;
77#[cfg(clt_turso_feature = "series")]
78mod series;
79mod stack;
80mod statement;
81mod stats;
82#[allow(dead_code)]
83#[cfg(clt_turso_feature = "time")]
84mod time;
85mod translate;
86mod util;
87#[cfg(clt_turso_feature = "uuid")]
88mod uuid;
89#[cfg(not(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench")))]
90mod vdbe;
91mod vtab;
92
93#[cfg(any(clt_turso_feature = "fuzz", clt_turso_feature = "bench"))]
94pub use function::MathFunc;
95
96use crate::{
97    busy::{BusyHandler, BusyHandlerCallback},
98    incremental::view::AllViewsTxState,
99    index_method::IndexMethod,
100    progress::ProgressHandler,
101    schema::Trigger,
102    stats::refresh_analyze_stats,
103    storage::{
104        checksum::CHECKSUM_REQUIRED_RESERVED_BYTES,
105        encryption::{AtomicCipherMode, SQLITE_HEADER, TURSO_HEADER_PREFIX},
106        journal_mode,
107        pager::{self, AutoVacuumMode, HeaderRef, HeaderRefMut},
108        sqlite3_ondisk::{RawVersion, TextEncoding, Version},
109    },
110    sync::{
111        atomic::{
112            AtomicBool, AtomicI32, AtomicI64, AtomicIsize, AtomicU16, AtomicU64, AtomicU8,
113            AtomicUsize, Ordering,
114        },
115        Arc, LazyLock, Mutex, RwLock, Weak,
116    },
117    translate::{emitter::TransactionMode, pragma::TURSO_CDC_DEFAULT_TABLE_NAME},
118    vdbe::metrics::ConnectionMetrics,
119    vtab::VirtualTable,
120};
121use arc_swap::{ArcSwap, ArcSwapOption};
122use core::str;
123use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
124use schema::Schema;
125#[cfg(host_shared_wal)]
126use std::path::Path;
127#[cfg(host_shared_wal)]
128use std::sync::OnceLock;
129use std::{
130    fmt::{self},
131    ops::Deref,
132    time::Duration,
133};
134#[cfg(clt_turso_feature = "fs")]
135use storage::database::DatabaseFile;
136#[cfg(host_shared_wal)]
137use storage::shared_wal_coordination::MappedSharedWalCoordination;
138use storage::{page_cache::PageCache, sqlite3_ondisk::PageSize};
139use tracing::{instrument, Level};
140use turso_macros::AtomicEnum;
141use turso_parser::{ast, ast::Cmd, parser::Parser};
142
143pub use connection::{resolve_ext_path, Connection, Row, StepResult, SymbolTable};
144pub(crate) use connection::{AtomicTransactionState, TransactionState};
145pub use error::{io_error, CompletionError, LimboError};
146pub use function::ContextCollationFunction;
147#[cfg(clt_turso_feature = "io_memory_yield")]
148pub use io::MemoryYieldIO;
149#[cfg(all(clt_turso_feature = "fs", target_family = "unix", not(miri)))]
150pub use io::UnixIO;
151#[cfg(all(clt_turso_feature = "fs", target_os = "linux", clt_turso_feature = "io_uring", not(miri)))]
152pub use io::UringIO;
153#[cfg(all(
154    clt_turso_feature = "fs",
155    target_os = "windows",
156    clt_turso_feature = "experimental_win_iocp",
157    not(miri)
158))]
159pub use io::WindowsIOCP;
160pub use io::{
161    clock::{Clock, MonotonicInstant, WallClockInstant},
162    get_registered_io, list_registered_io, register_io, unregister_io, Buffer, Completion,
163    CompletionType, File, GroupCompletion, MemoryIO, OpenFlags, PlatformIO, SharedBufferData,
164    SyscallIO, WriteCompletion, IO,
165};
166pub use numeric::{nonnan::NonNan, Numeric};
167pub use statement::{ColumnTypeInfo, ColumnTypeKind, Statement, StatementStatusCounter};
168pub use storage::{
169    buffer_pool::BufferPool,
170    database::{DatabaseStorage, IOContext},
171    encryption::{CipherMode, EncryptionContext, EncryptionKey},
172    pager::{Page, PageRef, Pager},
173    wal::{CheckpointMode, CheckpointResult, Wal, WalAutoActions, WalFile, WalFileShared},
174};
175pub use translate::expr::{walk_expr_mut, WalkControl};
176pub use turso_ext::ContextDestructor;
177pub use turso_macros::{
178    turso_assert, turso_assert_all, turso_assert_eq, turso_assert_greater_than,
179    turso_assert_greater_than_or_equal, turso_assert_less_than, turso_assert_less_than_or_equal,
180    turso_assert_ne, turso_assert_reachable, turso_assert_some, turso_assert_sometimes,
181    turso_assert_sometimes_greater_than, turso_assert_sometimes_greater_than_or_equal,
182    turso_assert_sometimes_less_than, turso_assert_sometimes_less_than_or_equal,
183    turso_assert_unreachable, turso_debug_assert, turso_soft_unreachable,
184};
185use types::IOCompletions;
186pub use types::{IOResult, Value, ValueRef};
187pub use util::IOExt;
188pub use vdbe::{
189    builder::QueryMode, explain::EXPLAIN_COLUMNS, explain::EXPLAIN_QUERY_PLAN_COLUMNS,
190    FromValueRow, PrepareContext, PreparedProgram, Program, Register,
191};
192pub use vtab::{InternalVirtualTable, InternalVirtualTableCursor};
193
194/// Database index for the main database (always 0 in SQLite).
195pub const MAIN_DB_ID: usize = 0;
196
197mod turso_types_vtab;
198
199/// Database index for the temp database (always 1 in SQLite).
200pub const TEMP_DB_ID: usize = 1;
201
202/// First database index used for ATTACH-ed databases.
203/// SQLite reserves 0 for "main" and 1 for "temp", so attached databases
204/// start at index 2.
205pub const FIRST_ATTACHED_DB_ID: usize = 2;
206
207/// Sentinel used when a SQL schema qualifier references an attached
208/// database name that cannot be resolved against the current
209/// connection's attached catalog (e.g. after reloading a
210/// `CREATE TEMP TRIGGER tr ON aux.x ...` row from `temp.sqlite_schema`
211/// without `aux` being attached). Stored in
212/// `Trigger::target_database_id` so filters never accidentally match a
213/// real database. Never equal to any real db id — guaranteed by
214/// `usize::MAX`.
215pub const INVALID_DB_ID: usize = usize::MAX;
216
217/// Returns true if the database index refers to "main" or "temp"
218pub const fn is_main_or_temp_db(database_id: usize) -> bool {
219    database_id == MAIN_DB_ID || database_id == TEMP_DB_ID
220}
221
222/// Returns true if the database index refers to an attached database
223/// (i.e. not "main" and not "temp").
224pub const fn is_attached_db(database_id: usize) -> bool {
225    database_id >= FIRST_ATTACHED_DB_ID
226}
227
228/// Configuration for database features
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230pub struct DatabaseOpts {
231    pub enable_views: bool,
232    pub enable_custom_types: bool,
233    pub enable_encryption: bool,
234    pub enable_index_method: bool,
235    pub enable_autovacuum: bool,
236    pub enable_vacuum: bool,
237    pub enable_attach: bool,
238    pub enable_generated_columns: bool,
239    pub enable_multiprocess_wal: bool,
240    pub enable_without_rowid: bool,
241    pub enable_experimental_mvcc_passive_checkpoint: bool,
242    pub unsafe_testing: bool,
243    enable_load_extension: bool,
244}
245
246impl DatabaseOpts {
247    pub fn new() -> Self {
248        Self::default()
249    }
250
251    #[cfg(clt_turso_feature = "cli_only")]
252    pub fn turso_cli(mut self) -> Self {
253        self.enable_load_extension = true;
254        self
255    }
256
257    pub fn with_views(mut self, enable: bool) -> Self {
258        self.enable_views = enable;
259        self
260    }
261
262    pub fn with_custom_types(mut self, enable: bool) -> Self {
263        self.enable_custom_types = enable;
264        self
265    }
266
267    pub fn with_encryption(mut self, enable: bool) -> Self {
268        self.enable_encryption = enable;
269        self
270    }
271
272    pub fn with_index_method(mut self, enable: bool) -> Self {
273        self.enable_index_method = enable;
274        self
275    }
276
277    pub fn with_autovacuum(mut self, enable: bool) -> Self {
278        self.enable_autovacuum = enable;
279        self
280    }
281
282    pub fn with_vacuum(mut self, enable: bool) -> Self {
283        self.enable_vacuum = enable;
284        self
285    }
286
287    pub fn with_experimental_mvcc_passive_checkpoint(mut self, enable: bool) -> Self {
288        self.enable_experimental_mvcc_passive_checkpoint = enable;
289        self
290    }
291
292    pub fn with_attach(mut self, enable: bool) -> Self {
293        self.enable_attach = enable;
294        self
295    }
296
297    pub fn with_generated_columns(mut self, enable: bool) -> Self {
298        self.enable_generated_columns = enable;
299        self
300    }
301
302    pub fn with_multiprocess_wal(mut self, enable: bool) -> Self {
303        self.enable_multiprocess_wal = enable;
304        self
305    }
306
307    pub fn with_without_rowid(mut self, enable: bool) -> Self {
308        self.enable_without_rowid = enable;
309        self
310    }
311
312    pub fn with_unsafe_testing(mut self, enable: bool) -> Self {
313        self.unsafe_testing = enable;
314        self
315    }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum SharedWalCoordinationOpenTelemetryMode {
320    Exclusive,
321    MultiProcess,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub struct SharedWalOpenTelemetry {
326    pub loaded_from_disk_scan: bool,
327    pub reopened_max_frame: u64,
328    pub reopened_nbackfills: u64,
329    pub reopened_checkpoint_seq: u32,
330    pub coordination_open_mode: Option<SharedWalCoordinationOpenTelemetryMode>,
331    pub sanitized_backfill_proof_on_open: bool,
332}
333
334#[cfg(clt_turso_feature = "simulator")]
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub struct SharedWalTestingSnapshot {
337    pub max_frame: u64,
338    pub nbackfills: u64,
339    pub checkpoint_seq: u32,
340    pub frame_index_overflowed: bool,
341}
342
343#[derive(Clone, Debug, Default)]
344pub struct EncryptionOpts {
345    pub cipher: String,
346    pub hexkey: String,
347}
348
349impl EncryptionOpts {
350    pub fn new() -> Self {
351        Self::default()
352    }
353}
354
355pub type Result<T, E = LimboError> = std::result::Result<T, E>;
356
357#[derive(Debug, AtomicEnum, Clone, Copy, PartialEq, Eq)]
358pub enum SyncMode {
359    Off = 0,
360    Normal = 1,
361    Full = 2,
362}
363
364/// Control where temporary tables and indices are stored.
365/// Matches SQLite's PRAGMA temp_store values:
366/// - 0 = DEFAULT (use compile-time default, which is FILE)
367/// - 1 = FILE (always use temp files on disk)
368/// - 2 = MEMORY (always use in-memory storage)
369#[derive(Debug, AtomicEnum, Clone, Copy, PartialEq, Eq, Default)]
370pub enum TempStore {
371    #[default]
372    Default = 0,
373    File = 1,
374    Memory = 2,
375}
376
377pub(crate) type MvStore = mvcc::MvStore<mvcc::MvccClock, alloc::DynAllocator>;
378
379pub(crate) type MvCursor = mvcc::cursor::MvccLazyCursor<mvcc::MvccClock, alloc::DynAllocator>;
380
381/// Returns true for in memory databases (i.e. databases backed by MemoryIO)
382///
383/// Turso treats every path with the `:memory:` prefix as a named
384/// in-memory database.
385fn is_memory_like(path: &str) -> bool {
386    path.starts_with(":memory:") || path.starts_with("file::memory:") || path.is_empty()
387}
388
389/// Creates a read completion for database header reads that checks for short reads.
390/// The header is always on page 1, so this function hardcodes that page index.
391fn new_header_read_completion(buf: Arc<Buffer>) -> Completion {
392    let expected = buf.len();
393    Completion::new_read(buf, move |res| {
394        let Ok((_buf, bytes_read)) = res else {
395            return None; // IO error already captured in completion
396        };
397        if (bytes_read as usize) < expected {
398            tracing::error!(
399                "short read on database header: expected {expected} bytes, got {bytes_read}"
400            );
401            return Some(CompletionError::ShortRead {
402                page_idx: 1, // header is on page 1
403                expected,
404                actual: bytes_read as usize,
405            });
406        }
407        None
408    })
409}
410
411/// Phase tracking for async database opening
412#[derive(Default, Debug)]
413pub enum OpenDbAsyncPhase {
414    #[default]
415    Init,
416    /// Drives `Database::header_validation` (header validation + WAL recovery)
417    /// as a sub state machine so WAL recovery on open does not block.
418    ValidatingHeader,
419    ReadingHeader,
420    LoadingSchema,
421    BootstrapMvStore,
422    Done,
423}
424
425/// Sub state machine for [`Database::header_validation`], driven from
426/// [`OpenDbAsyncPhase::ValidatingHeader`]. Keeps WAL recovery on open
427/// non-blocking by yielding through its IO instead of `io.block`.
428/// Non-blocking read of the 512-byte database file header. Used by
429/// [`Database::init_pager`] to recover page size + reserved bytes without
430/// blocking on open.
431#[derive(Default)]
432pub(crate) enum DbHeaderReadState {
433    #[default]
434    Start,
435    Reading {
436        buf: Arc<Buffer>,
437        completion: Completion,
438    },
439}
440
441/// Sub state machine for [`Database::_init`], driven from
442/// [`HeaderValidationState::Start`]. Builds the `Pager` (reading page-size /
443/// reserved bytes from the DB header), begins a read transaction, then reads
444/// page 1 to determine the autovacuum mode — all without blocking.
445#[derive(Default)]
446pub(crate) enum InitState {
447    #[default]
448    Start,
449    /// Driving `init_pager` (its only IO is the DB-header read).
450    InitPager(DbHeaderReadState),
451    /// Pager built and read-tx open; reading page 1 for the autovacuum mode.
452    ReadPage1 { pager: Box<Pager> },
453}
454
455/// Sub state machine for [`Database::header_validation`], driven from
456/// [`OpenDbAsyncPhase::ValidatingHeader`]. Keeps WAL recovery on open
457/// non-blocking by yielding through its IO instead of `io.block`.
458enum HeaderValidationState {
459    Start {
460        init: InitState,
461    },
462    /// Pager created; (re-entrant) header reads + validation. Holds the owned
463    /// `Pager` because `set_wal` needs `&mut Pager`; it is `Arc`-wrapped only
464    /// once validation completes. `is_readonly`/`log_exists` are captured in
465    /// `Start` (before the autovacuum check may force ReadOnly) so re-entry
466    /// observes the original values.
467    Validate {
468        pager: Box<Pager>,
469        is_readonly: bool,
470        log_exists: bool,
471    },
472    /// A modified header (e.g. Legacy→WAL conversion) must be written to disk
473    /// before the WAL is attached. `completion` is the in-flight write.
474    WriteHeader {
475        pager: Box<Pager>,
476        page: PageRef,
477        open_mv_store: bool,
478        completion: Option<Completion>,
479    },
480    /// Open/recover the shared WAL. On non-host builds `driver` drives the
481    /// `OpenSharedWal` recovery scan; on host builds the WAL is produced
482    /// synchronously (native, where `io.step` pumps).
483    OpenWal {
484        pager: Box<Pager>,
485        open_mv_store: bool,
486        driver: Option<storage::wal::OpenSharedWal>,
487    },
488}
489
490impl Default for HeaderValidationState {
491    fn default() -> Self {
492        Self::Start {
493            init: InitState::default(),
494        }
495    }
496}
497
498/// State machine for async database opening
499pub struct OpenDbAsyncState {
500    phase: OpenDbAsyncPhase,
501    db: Option<Arc<Database>>,
502    pager: Option<Arc<Pager>>,
503    conn: Option<Arc<Connection>>,
504    encryption_key: Option<EncryptionKey>,
505    make_from_btree_state: schema::MakeFromBtreeState,
506    /// Schema lock held during LoadingSchema phase to ensure atomicity across IO yields
507    schema_guard: Option<sync::ArcMutexGuard<Arc<Schema>>>,
508    /// Registry key for insertion (computed once at start)
509    registry_key: Option<DatabaseKey>,
510    /// The database being built, held across the ValidatingHeader phase yields
511    /// before it is wrapped in an `Arc`.
512    building_db: Option<Database>,
513    /// Sub state machine for `header_validation`, driven in ValidatingHeader.
514    header_validation_state: HeaderValidationState,
515    /// The dedicated bootstrap connection used by `BootstrapMvStore`, held
516    /// across yields from `MvStore::bootstrap_nonblock`.
517    mvcc_bootstrap_conn: Option<Arc<Connection>>,
518    /// Sub state machine for `MvStore::bootstrap_nonblock`, driven in
519    /// `BootstrapMvStore`.
520    mvcc_bootstrap_state: mvcc::database::BootstrapState,
521}
522
523impl Default for OpenDbAsyncState {
524    fn default() -> Self {
525        Self::new()
526    }
527}
528
529impl OpenDbAsyncState {
530    pub fn new() -> Self {
531        Self {
532            phase: OpenDbAsyncPhase::Init,
533            db: None,
534            pager: None,
535            conn: None,
536            encryption_key: None,
537            make_from_btree_state: schema::MakeFromBtreeState::new(),
538            schema_guard: None,
539            registry_key: None,
540            building_db: None,
541            header_validation_state: HeaderValidationState::default(),
542            mvcc_bootstrap_conn: None,
543            mvcc_bootstrap_state: mvcc::database::BootstrapState::default(),
544        }
545    }
546}
547
548impl Drop for OpenDbAsyncState {
549    fn drop(&mut self) {
550        if let Some(registry_key) = self.registry_key.take() {
551            let mut registry = DATABASE_MANAGER.lock();
552            registry.remove(&registry_key);
553        }
554    }
555}
556
557/// Per-path entry in the database registry.
558enum RegistryEntry {
559    /// Another caller is currently opening this database. Callers that see
560    /// this should yield and retry later.
561    Opening,
562    /// The database has been opened and is (or was) live.
563    Ready(Weak<Database>),
564}
565
566/// The database manager ensures that there is a single, shared
567/// `Database` object per a database file. We need because it is not safe
568/// to have multiple independent WAL files open because coordination
569/// happens at process-level POSIX file advisory locks.
570///
571/// Uses parking_lot::Mutex instead of crate::sync::Mutex because this static
572/// must persist across shuttle test iterations. Shuttle resets its execution
573/// state between iterations, but static variables persist - using shuttle's
574/// Mutex here would cause panics when the second iteration tries to lock a
575/// mutex that belongs to a stale execution context.
576/// Registry key for the process-wide database manager.
577/// File-backed databases are keyed by their OS-level identity (dev, ino),
578/// matching SQLite's inodeList approach. Shared in-memory databases use
579/// their name as the key.
580///
581/// IMPORTANT: The mutex must only be held for brief HashMap operations, never
582/// across I/O yields. Holding it across yields deadlocks single-threaded
583/// event loops because the blocked thread
584/// can never resume the coroutine that owns the lock.
585#[derive(Debug, Clone, PartialEq, Eq, Hash)]
586enum DatabaseKey {
587    File(io::FileId),
588    SharedMemory(String),
589}
590
591#[allow(clippy::type_complexity)]
592static DATABASE_MANAGER: LazyLock<Arc<parking_lot::Mutex<HashMap<DatabaseKey, RegistryEntry>>>> =
593    LazyLock::new(|| Arc::new(parking_lot::Mutex::new(HashMap::default())));
594
595#[cfg(clt_turso_feature = "simulator")]
596pub fn clear_database_registry() {
597    DATABASE_MANAGER.lock().clear();
598}
599
600/// The `Database` object contains per database file state that is shared
601/// between multiple connections.
602///
603/// Do that `Database` object is cached and can be long lived. DO NOT store anything sensitive like
604/// encryption key here.
605pub struct Database<A: alloc::ConcurrentAllocator = alloc::DynAllocator> {
606    mv_store: ArcSwapOption<mvcc::MvStore<mvcc::MvccClock, A>>,
607    mv_store_allocator: A,
608    schema: Arc<Mutex<Arc<Schema>>>,
609    pub db_file: Arc<dyn DatabaseStorage>,
610    pub path: String,
611    wal_path: String,
612    pub io: Arc<dyn IO>,
613    buffer_pool: Arc<BufferPool>,
614    // Shared structures of a Database are the parts that are common to multiple threads that might
615    // create DB connections.
616    _shared_page_cache: Arc<RwLock<PageCache>>,
617
618    /// Optional per-database MVCC durable storage override.
619    ///
620    /// When set, MVCC will use this implementation for logical-log durability
621    /// (commit, sync, checkpoint thresholds, etc.) instead of the built-in storage.
622    durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
623    shared_wal: Arc<RwLock<WalFileShared>>,
624    #[cfg(host_shared_wal)]
625    shared_wal_coordination: OnceLock<Arc<MappedSharedWalCoordination>>,
626    init_lock: Arc<Mutex<()>>,
627    open_flags: OpenFlags,
628    // Use parking lot RwLock here and not `crate::sync::RwLock` because it relies on `data_ptr` and that is experimental
629    // in std.
630    builtin_syms: parking_lot::RwLock<SymbolTable>,
631    opts: DatabaseOpts,
632    n_connections: AtomicUsize,
633
634    /// In Memory Page 1 for Empty Dbs
635    init_page_1: Arc<ArcSwapOption<Page>>,
636
637    // Encryption
638    encryption_cipher_mode: AtomicCipherMode,
639}
640
641// SAFETY: This needs to be audited for thread safety.
642// See: https://github.com/tursodatabase/turso/issues/1552
643crate::assert::assert_send_sync!(Database);
644
645impl fmt::Debug for Database {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        let mut debug_struct = f.debug_struct("Database");
648        debug_struct
649            .field("path", &self.path)
650            .field("open_flags", &self.open_flags);
651
652        // Database state information
653        let db_state_value = match &*self.init_page_1.load() {
654            // If init_page1 exists, this means the DB is empty
655            Some(_) => "uninitialized",
656            None => "initialized",
657        };
658        debug_struct.field("db_state", &db_state_value);
659
660        let mv_store_status = if self.get_mv_store().is_some() {
661            "present"
662        } else {
663            "none"
664        };
665        debug_struct.field("mv_store", &mv_store_status);
666
667        let init_lock_status = if self.init_lock.try_lock().is_some() {
668            "unlocked"
669        } else {
670            "locked"
671        };
672        debug_struct.field("init_lock", &init_lock_status);
673
674        let wal_status = match self.shared_wal.try_read() {
675            Some(wal) if wal.metadata.enabled.load(Ordering::SeqCst) => "enabled",
676            Some(_) => "disabled",
677            None => "locked_for_write",
678        };
679        debug_struct.field("wal_state", &wal_status);
680
681        // Page cache info (just basic stats, not full contents)
682        let cache_info = match self._shared_page_cache.try_read() {
683            Some(cache) => format!("( capacity {}, used: {} )", cache.capacity(), cache.len()),
684            None => "locked".to_string(),
685        };
686        debug_struct.field("page_cache", &cache_info);
687
688        debug_struct.field(
689            "n_connections",
690            &self
691                .n_connections
692                .load(crate::sync::atomic::Ordering::SeqCst),
693        );
694        debug_struct.finish()
695    }
696}
697
698impl Database {
699    /// Returns true if this database is backed by MemoryIO.
700    pub fn is_in_memory_db(&self) -> bool {
701        is_memory_like(&self.path)
702    }
703
704    #[allow(clippy::too_many_arguments)]
705    fn new(
706        opts: DatabaseOpts,
707        flags: OpenFlags,
708        path: impl Into<String>,
709        wal_path: impl Into<String>,
710        io: &Arc<dyn IO>,
711        db_file: Arc<dyn DatabaseStorage>,
712        encryption_opts: Option<EncryptionOpts>,
713        mv_store_allocator: alloc::DynAllocator,
714    ) -> Result<Self> {
715        let path = path.into();
716        let wal_path = wal_path.into();
717        let shared_wal = WalFileShared::new_noop();
718        let mv_store = ArcSwapOption::empty();
719
720        let db_size = db_file.size()?;
721
722        let shared_page_cache = Arc::new(RwLock::new(PageCache::default()));
723        let syms = SymbolTable::new();
724        let arena_size = if std::env::var("TESTING").is_ok_and(|v| v.eq_ignore_ascii_case("true")) {
725            BufferPool::TEST_ARENA_SIZE
726        } else {
727            BufferPool::DEFAULT_ARENA_SIZE
728        };
729
730        let encryption_cipher_mode = if let Some(encryption_opts) = encryption_opts {
731            Some(CipherMode::try_from(encryption_opts.cipher.as_str())?)
732        } else {
733            None
734        };
735
736        let init_page_1 = if db_size == 0 {
737            let default_page_1 = pager::default_page1(encryption_cipher_mode.as_ref());
738
739            Some(default_page_1)
740        } else {
741            None
742        };
743
744        let db = Database {
745            mv_store,
746            mv_store_allocator,
747            path,
748            wal_path,
749            schema: Arc::new(Mutex::new(Arc::new({
750                let mut s = Schema::with_options(opts.enable_custom_types)?;
751                s.generated_columns_enabled = opts.enable_generated_columns;
752                s
753            }))),
754            _shared_page_cache: shared_page_cache,
755            shared_wal,
756            #[cfg(host_shared_wal)]
757            shared_wal_coordination: OnceLock::new(),
758            db_file,
759            builtin_syms: parking_lot::RwLock::new(syms),
760            io: io.clone(),
761            open_flags: flags,
762            init_lock: Arc::new(Mutex::new(())),
763            opts,
764            buffer_pool: BufferPool::begin_init(io, arena_size),
765            n_connections: AtomicUsize::new(0),
766
767            init_page_1: Arc::new(ArcSwapOption::new(init_page_1)),
768
769            encryption_cipher_mode: AtomicCipherMode::new(
770                encryption_cipher_mode.unwrap_or(CipherMode::None),
771            ),
772
773            durable_storage: None,
774        };
775
776        db.register_global_builtin_extensions()
777            .expect("unable to register global extensions");
778        Ok(db)
779    }
780
781    #[cfg(clt_turso_feature = "fs")]
782    pub fn open_file(io: Arc<dyn IO>, path: &str) -> Result<Arc<Database>> {
783        Self::open_file_with_flags(io, path, OpenFlags::default(), DatabaseOpts::new(), None)
784    }
785
786    /// Open or retrieve a shared named in-memory database.
787    /// Multiple connections to the same `name` share a single `Database`,
788    /// matching SQLite's `file:name?mode=memory&cache=shared` semantics.
789    #[cfg(clt_turso_feature = "fs")]
790    pub fn open_shared_memory(name: &str) -> Result<Arc<Database>> {
791        let key = DatabaseKey::SharedMemory(name.to_string());
792
793        {
794            let registry = DATABASE_MANAGER.lock();
795            if let Some(RegistryEntry::Ready(weak)) = registry.get(&key) {
796                if let Some(db) = weak.upgrade() {
797                    return Ok(db);
798                }
799            }
800        }
801        // `:memory:` paths bypass DATABASE_MANAGER internally, so no deadlock.
802        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
803        let db = Self::open_file(io, ":memory:")?;
804
805        let mut registry = DATABASE_MANAGER.lock();
806        if let Some(RegistryEntry::Ready(weak)) = registry.get(&key) {
807            if let Some(existing) = weak.upgrade() {
808                return Ok(existing);
809            }
810        }
811        registry.insert(key, RegistryEntry::Ready(Arc::downgrade(&db)));
812        Ok(db)
813    }
814
815    #[cfg(clt_turso_feature = "fs")]
816    #[cfg(host_shared_wal)]
817    fn effective_open_flags_for_path(
818        io: &Arc<dyn IO>,
819        path: &str,
820        flags: OpenFlags,
821        opts: DatabaseOpts,
822    ) -> Result<OpenFlags> {
823        if !opts.enable_multiprocess_wal {
824            return Ok(flags);
825        }
826
827        if is_memory_like(path) {
828            return Err(LimboError::InvalidArgument(format!(
829                "experimental multiprocess WAL is not supported for in-memory database path '{path}'"
830            )));
831        }
832        if !io.supports_shared_wal_coordination() {
833            return Err(LimboError::InvalidArgument(format!(
834                "experimental multiprocess WAL is not supported by the active IO backend for '{path}'"
835            )));
836        }
837        if !Self::path_allows_shared_wal_coordination(Path::new(path))? {
838            return Err(LimboError::InvalidArgument(format!(
839                "experimental multiprocess WAL is not supported on the filesystem backing '{path}'"
840            )));
841        }
842
843        if !flags.contains(OpenFlags::ReadOnly) {
844            return Ok(flags | OpenFlags::NoLock);
845        }
846
847        Ok(flags)
848    }
849
850    #[cfg(clt_turso_feature = "fs")]
851    #[cfg(not(host_shared_wal))]
852    fn effective_open_flags_for_path(
853        _io: &Arc<dyn IO>,
854        _path: &str,
855        flags: OpenFlags,
856        _opts: DatabaseOpts,
857    ) -> Result<OpenFlags> {
858        // On unsupported platforms, keep the flag as a no-op so generic
859        // cross-platform helpers/tests can request multiprocess WAL without
860        // breaking legacy single-process behavior.
861        Ok(flags)
862    }
863
864    #[cfg(clt_turso_feature = "fs")]
865    #[cfg(host_shared_wal)]
866    fn reject_live_multiprocess_wal_for_legacy_open(
867        io: &Arc<dyn IO>,
868        path: &str,
869        opts: DatabaseOpts,
870    ) -> Result<()> {
871        if opts.enable_multiprocess_wal
872            || is_memory_like(path)
873            || !io.supports_shared_wal_coordination()
874            || !Self::path_allows_shared_wal_coordination(Path::new(path))?
875        {
876            return Ok(());
877        }
878
879        let coordination_path =
880            storage::wal::coordination_path_for_wal_path(&format!("{path}-wal"));
881        let Some(authority) =
882            MappedSharedWalCoordination::open_existing(io, Path::new(&coordination_path), 64)?
883        else {
884            return Ok(());
885        };
886
887        if matches!(
888            authority.open_mode(),
889            storage::shared_wal_coordination::SharedWalCoordinationOpenMode::MultiProcess
890        ) {
891            return Err(LimboError::LockingError(format!(
892                "Failed opening database '{path}'. Database is already open with experimental multiprocess WAL in another process"
893            )));
894        }
895
896        Ok(())
897    }
898
899    #[cfg(clt_turso_feature = "fs")]
900    #[cfg(not(host_shared_wal))]
901    fn reject_live_multiprocess_wal_for_legacy_open(
902        _io: &Arc<dyn IO>,
903        _path: &str,
904        _opts: DatabaseOpts,
905    ) -> Result<()> {
906        Ok(())
907    }
908
909    #[cfg(clt_turso_feature = "fs")]
910    #[cfg(host_shared_wal)]
911    fn reject_live_legacy_wal_for_multiprocess_open(
912        io: &Arc<dyn IO>,
913        path: &str,
914        flags: OpenFlags,
915        opts: DatabaseOpts,
916    ) -> Result<()> {
917        if !opts.enable_multiprocess_wal || flags.contains(OpenFlags::ReadOnly) {
918            return Ok(());
919        }
920
921        let probe_flags = (flags | OpenFlags::Create) & !OpenFlags::NoLock & !OpenFlags::ReadOnly;
922        match io.open_file(path, probe_flags, true) {
923            Ok(_probe_file) => Ok(()),
924            Err(LimboError::LockingError(_)) => Err(LimboError::LockingError(format!(
925                "Failed opening database '{path}'. Database is already open without experimental multiprocess WAL in another process"
926            ))),
927            Err(err) => Err(err),
928        }
929    }
930
931    #[cfg(clt_turso_feature = "fs")]
932    #[cfg(not(host_shared_wal))]
933    fn reject_live_legacy_wal_for_multiprocess_open(
934        _io: &Arc<dyn IO>,
935        _path: &str,
936        _flags: OpenFlags,
937        _opts: DatabaseOpts,
938    ) -> Result<()> {
939        Ok(())
940    }
941
942    /// Look up a database in the process-wide registry by file identity.
943    /// Returns the cached Database if found, with encryption validation.
944    /// This avoids opening a file (and acquiring a file lock) when the
945    /// database is already open in this process.
946    fn lookup_in_registry(
947        path: &str,
948        encryption_opts: &Option<EncryptionOpts>,
949    ) -> Result<Option<Arc<Database>>> {
950        if is_memory_like(path) {
951            return Ok(None);
952        }
953        let file_id = match io::get_file_id(path) {
954            Ok(id) => id,
955            Err(_) => return Ok(None), // file doesn't exist yet
956        };
957        let key = DatabaseKey::File(file_id);
958        let registry = DATABASE_MANAGER.lock();
959        let db = match registry.get(&key) {
960            Some(RegistryEntry::Ready(weak)) => match weak.upgrade() {
961                Some(db) => db,
962                None => return Ok(None),
963            },
964            _ => return Ok(None),
965        };
966
967        // Validate encryption compatibility (key is not stored for security,
968        // so we can only check cipher mode)
969        let db_is_encrypted = !matches!(db.encryption_cipher_mode.get(), CipherMode::None);
970        if db_is_encrypted && encryption_opts.is_none() {
971            return Err(LimboError::InvalidArgument(
972                "Database is encrypted but no encryption options provided".to_string(),
973            ));
974        }
975
976        Ok(Some(db))
977    }
978
979    #[cfg(clt_turso_feature = "fs")]
980    pub fn open_file_with_flags(
981        io: Arc<dyn IO>,
982        path: &str,
983        flags: OpenFlags,
984        opts: DatabaseOpts,
985        encryption_opts: Option<EncryptionOpts>,
986    ) -> Result<Arc<Database>> {
987        Self::open_file_with_flags_and_durable_storage(io, path, flags, opts, encryption_opts, None)
988    }
989
990    #[cfg(clt_turso_feature = "fs")]
991    pub fn open_file_with_flags_and_durable_storage(
992        io: Arc<dyn IO>,
993        path: &str,
994        flags: OpenFlags,
995        opts: DatabaseOpts,
996        encryption_opts: Option<EncryptionOpts>,
997        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
998    ) -> Result<Arc<Database>> {
999        // Check the registry before opening the file to avoid acquiring a file
1000        // lock that would conflict with an already-open Database in this process.
1001        if let Some(db) = Self::lookup_in_registry(path, &encryption_opts)? {
1002            if durable_storage.is_some() && db.durable_storage.is_none() {
1003                return Err(LimboError::InvalidArgument(
1004                    "database already open without custom durable storage; \
1005                     close the existing instance before reopening with a custom DurableStorage"
1006                        .to_string(),
1007                ));
1008            }
1009            return Ok(db);
1010        }
1011        // Mixed legacy/multiprocess opens are incompatible, but the two modes
1012        // advertise themselves through different lock domains (`.tshm` vs DB
1013        // file lock). We therefore probe both directions around the actual file
1014        // open to narrow the TOCTOU window:
1015        //
1016        // 1. legacy open rejects an already-live multiprocess authority
1017        Self::reject_live_multiprocess_wal_for_legacy_open(&io, path, opts)?;
1018        let effective_flags = Self::effective_open_flags_for_path(&io, path, flags, opts)?;
1019
1020        // 2. multiprocess open rejects an already-live legacy DB-file lock
1021        Self::reject_live_legacy_wal_for_multiprocess_open(&io, path, flags, opts)?;
1022        let file = io.open_file(path, effective_flags, true)?;
1023
1024        // 3. legacy open re-checks after `open_file()` in case a multiprocess
1025        //    authority appeared between the initial probe and the actual open
1026        Self::reject_live_multiprocess_wal_for_legacy_open(&io, path, opts)?;
1027        let db_file = Arc::new(DatabaseFile::new(file));
1028        Self::open_with_flags(
1029            io,
1030            path,
1031            db_file,
1032            effective_flags,
1033            opts,
1034            encryption_opts,
1035            durable_storage,
1036        )
1037    }
1038
1039    pub fn open(
1040        io: Arc<dyn IO>,
1041        path: &str,
1042        db_file: Arc<dyn DatabaseStorage>,
1043    ) -> Result<Arc<Database>> {
1044        Self::open_with_flags(
1045            io,
1046            path,
1047            db_file,
1048            OpenFlags::default(),
1049            DatabaseOpts::new(),
1050            None,
1051            None,
1052        )
1053    }
1054
1055    #[allow(clippy::too_many_arguments)]
1056    pub fn open_with_flags(
1057        io: Arc<dyn IO>,
1058        path: &str,
1059        db_file: Arc<dyn DatabaseStorage>,
1060        flags: OpenFlags,
1061        opts: DatabaseOpts,
1062        encryption_opts: Option<EncryptionOpts>,
1063        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1064    ) -> Result<Arc<Database>> {
1065        Self::open_with_flags_with_allocator(
1066            io,
1067            path,
1068            db_file,
1069            flags,
1070            opts,
1071            encryption_opts,
1072            durable_storage,
1073            alloc::DynAllocator::default(),
1074        )
1075    }
1076
1077    #[allow(clippy::too_many_arguments)]
1078    pub fn open_with_flags_with_allocator(
1079        io: Arc<dyn IO>,
1080        path: &str,
1081        db_file: Arc<dyn DatabaseStorage>,
1082        flags: OpenFlags,
1083        opts: DatabaseOpts,
1084        encryption_opts: Option<EncryptionOpts>,
1085        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1086        allocator: alloc::DynAllocator,
1087    ) -> Result<Arc<Database>> {
1088        let mut state = OpenDbAsyncState::new();
1089        loop {
1090            match Self::open_with_flags_async_with_allocator(
1091                &mut state,
1092                io.clone(),
1093                path,
1094                db_file.clone(),
1095                flags,
1096                opts,
1097                encryption_opts.clone(),
1098                durable_storage.clone(),
1099                allocator.clone(),
1100            )? {
1101                IOResult::Done(db) => return Ok(db),
1102                IOResult::IO(io_completion) => {
1103                    io_completion.wait(&*io)?;
1104                }
1105            }
1106        }
1107    }
1108
1109    /// async flow of opening the database
1110    /// this is important to have open async, otherwise sync-engine will not work properly for cases when schema table span multiple pages
1111    /// (so, potentially network IO is needed to load them)
1112    ///
1113    /// Uses the database registry to ensure single Database instance per file within a process.
1114    /// Caller must drive the IO loop and pass state between calls.
1115    /// An `Opening` sentinel in the registry prevents concurrent opens of the same path
1116    /// without holding the mutex across I/O yields.
1117    #[allow(clippy::too_many_arguments)]
1118    pub fn open_with_flags_async(
1119        state: &mut OpenDbAsyncState,
1120        io: Arc<dyn IO>,
1121        path: &str,
1122        db_file: Arc<dyn DatabaseStorage>,
1123        flags: OpenFlags,
1124        opts: DatabaseOpts,
1125        encryption_opts: Option<EncryptionOpts>,
1126        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1127    ) -> Result<IOResult<Arc<Database>>> {
1128        // Re-derive lock-mode flags from opts the same way the sync
1129        // `open_file_with_flags` path does: multiprocess WAL must open the
1130        // WAL file with NoLock or the second process fails to lock `-wal`.
1131        #[cfg(clt_turso_feature = "fs")]
1132        let flags = Self::effective_open_flags_for_path(&io, path, flags, opts)?;
1133        Self::open_with_flags_async_with_allocator(
1134            state,
1135            io,
1136            path,
1137            db_file,
1138            flags,
1139            opts,
1140            encryption_opts,
1141            durable_storage,
1142            alloc::DynAllocator::default(),
1143        )
1144    }
1145
1146    #[allow(clippy::too_many_arguments)]
1147    pub fn open_with_flags_async_with_allocator(
1148        state: &mut OpenDbAsyncState,
1149        io: Arc<dyn IO>,
1150        path: &str,
1151        db_file: Arc<dyn DatabaseStorage>,
1152        flags: OpenFlags,
1153        opts: DatabaseOpts,
1154        encryption_opts: Option<EncryptionOpts>,
1155        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1156        allocator: alloc::DynAllocator,
1157    ) -> Result<IOResult<Arc<Database>>> {
1158        let result = Self::open_with_flags_async_internal(
1159            state,
1160            io,
1161            path,
1162            db_file,
1163            flags,
1164            opts,
1165            encryption_opts,
1166            durable_storage,
1167            allocator,
1168        );
1169        if result.is_err() {
1170            // On error, remove the Opening sentinel so other callers can proceed.
1171            if let Some(registry_key) = state.registry_key.take() {
1172                let mut registry = DATABASE_MANAGER.lock();
1173                registry.remove(&registry_key);
1174            }
1175        }
1176        result
1177    }
1178
1179    #[allow(clippy::too_many_arguments)]
1180    fn open_with_flags_async_internal(
1181        state: &mut OpenDbAsyncState,
1182        io: Arc<dyn IO>,
1183        path: &str,
1184        db_file: Arc<dyn DatabaseStorage>,
1185        flags: OpenFlags,
1186        opts: DatabaseOpts,
1187        encryption_opts: Option<EncryptionOpts>,
1188        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1189        allocator: alloc::DynAllocator,
1190    ) -> Result<IOResult<Arc<Database>>> {
1191        // turso-sync-engine creates 2 databases with different names in the same IO if MemoryIO is used
1192        // in this case we need to bypass registry (as this is MemoryIO DB) but also preserve original distinction in names (e.g. :memory:-draft and :memory:-synced)
1193        // so, we bypass registry for all in memory dbs (i.e. db paths which starts with ":memory:")
1194
1195        if matches!(state.phase, OpenDbAsyncPhase::Init) && !is_memory_like(path) {
1196            // Briefly lock the registry to check/reserve — never hold across I/O yields.
1197            let mut registry = DATABASE_MANAGER.lock();
1198
1199            // Look up by file identity (dev, ino). If file doesn't exist
1200            // yet (CREATE mode), skip lookup — no cached entry is possible.
1201            if let Ok(file_id) = io.file_id(path) {
1202                let key = DatabaseKey::File(file_id);
1203                match registry.get(&key) {
1204                    Some(RegistryEntry::Ready(weak)) => {
1205                        if let Some(db) = weak.upgrade() {
1206                            tracing::debug!("took database {path:?} from the registry");
1207
1208                            let db_is_encrypted =
1209                                !matches!(db.encryption_cipher_mode.get(), CipherMode::None);
1210                            if db_is_encrypted && encryption_opts.is_none() {
1211                                return Err(LimboError::InvalidArgument(
1212                                    "Database is encrypted but no encryption options provided"
1213                                        .to_string(),
1214                                ));
1215                            }
1216                            return Ok(IOResult::Done(db));
1217                        }
1218                        // Weak ref expired — treat as absent, fall through to insert Opening.
1219                        registry.insert(key.clone(), RegistryEntry::Opening);
1220                    }
1221                    Some(RegistryEntry::Opening) => {
1222                        // Another caller is already opening this path. Yield so the
1223                        // event loop can make progress and we retry later.
1224                        return Ok(IOResult::IO(types::IOCompletions::Single(
1225                            io::Completion::new_yield(),
1226                        )));
1227                    }
1228                    None => {
1229                        // Not in registry — mark as Opening and proceed.
1230                        registry.insert(key.clone(), RegistryEntry::Opening);
1231                    }
1232                }
1233                state.registry_key = Some(key);
1234            }
1235            // Lock is dropped here — the Opening sentinel prevents concurrent opens
1236            // of the same path without holding the mutex across yields.
1237        }
1238
1239        // Open the database asynchronously (no registry lock held).
1240        let result = Self::open_with_flags_bypass_registry_async_with_allocator(
1241            state,
1242            io.clone(),
1243            path,
1244            None,
1245            db_file,
1246            flags,
1247            opts,
1248            encryption_opts,
1249            durable_storage,
1250            allocator,
1251        )?;
1252
1253        if let IOResult::Done(ref db) = result {
1254            // Register the opened database and remove the Opening sentinel.
1255            if let Some(registry_key) = state.registry_key.take() {
1256                let mut registry = DATABASE_MANAGER.lock();
1257                registry.insert(registry_key, RegistryEntry::Ready(Arc::downgrade(db)));
1258            }
1259        }
1260
1261        Ok(result)
1262    }
1263
1264    #[allow(clippy::too_many_arguments)]
1265    fn open_with_flags_bypass_registry_async_with_allocator(
1266        state: &mut OpenDbAsyncState,
1267        io: Arc<dyn IO>,
1268        path: &str,
1269        wal_path: Option<&str>,
1270        db_file: Arc<dyn DatabaseStorage>,
1271        flags: OpenFlags,
1272        opts: DatabaseOpts,
1273        encryption_opts: Option<EncryptionOpts>,
1274        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1275        allocator: alloc::DynAllocator,
1276    ) -> Result<IOResult<Arc<Database>>> {
1277        let result = Self::open_with_flags_bypass_registry_async_internal(
1278            state,
1279            io,
1280            path,
1281            wal_path,
1282            db_file,
1283            flags,
1284            opts,
1285            encryption_opts,
1286            durable_storage,
1287            allocator,
1288        );
1289        if result.is_err() {
1290            let _ = state.schema_guard.take();
1291        }
1292        result
1293    }
1294
1295    /// method for tests - for all other code we must use async alternative
1296    #[cfg(all(clt_turso_feature = "fs", clt_turso_feature = "conn_raw_api"))]
1297    pub fn open_with_flags_bypass_registry(
1298        io: Arc<dyn IO>,
1299        path: &str,
1300        wal_path: &str,
1301        db_file: Arc<dyn DatabaseStorage>,
1302        flags: OpenFlags,
1303        opts: DatabaseOpts,
1304        encryption_opts: Option<EncryptionOpts>,
1305    ) -> Result<Arc<Database>> {
1306        let mut state = OpenDbAsyncState::new();
1307        loop {
1308            match Self::open_with_flags_bypass_registry_async(
1309                &mut state,
1310                io.clone(),
1311                path,
1312                Some(wal_path),
1313                db_file.clone(),
1314                flags,
1315                opts,
1316                encryption_opts.clone(),
1317                None,
1318            )? {
1319                IOResult::Done(db) => return Ok(db),
1320                IOResult::IO(io_completion) => {
1321                    io_completion.wait(&*io)?;
1322                }
1323            }
1324        }
1325    }
1326
1327    /// Async version of database opening that returns IOResult.
1328    /// Caller must drive the IO loop and pass state between calls.
1329    /// This is useful for sync engine which needs to yield on IO.
1330    #[allow(clippy::too_many_arguments)]
1331    pub fn open_with_flags_bypass_registry_async(
1332        state: &mut OpenDbAsyncState,
1333        io: Arc<dyn IO>,
1334        path: &str,
1335        wal_path: Option<&str>,
1336        db_file: Arc<dyn DatabaseStorage>,
1337        flags: OpenFlags,
1338        opts: DatabaseOpts,
1339        encryption_opts: Option<EncryptionOpts>,
1340        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1341    ) -> Result<IOResult<Arc<Database>>> {
1342        let result = Self::open_with_flags_bypass_registry_async_internal(
1343            state,
1344            io,
1345            path,
1346            wal_path,
1347            db_file,
1348            flags,
1349            opts,
1350            encryption_opts,
1351            durable_storage,
1352            alloc::DynAllocator::default(),
1353        );
1354        if result.is_err() {
1355            // schema_guard is set by the open_with_flags_bypass_registry_async_internal - so we release it in case of error
1356            // registry_guard is not managed by this function - so we don't touch it here and reset in the appropriate place
1357            let _ = state.schema_guard.take();
1358        }
1359        result
1360    }
1361
1362    #[allow(clippy::too_many_arguments)]
1363    fn open_with_flags_bypass_registry_async_internal(
1364        state: &mut OpenDbAsyncState,
1365        io: Arc<dyn IO>,
1366        path: &str,
1367        wal_path: Option<&str>,
1368        db_file: Arc<dyn DatabaseStorage>,
1369        flags: OpenFlags,
1370        opts: DatabaseOpts,
1371        encryption_opts: Option<EncryptionOpts>,
1372        durable_storage: Option<Arc<dyn crate::mvcc::persistent_storage::DurableStorage>>,
1373        allocator: alloc::DynAllocator,
1374    ) -> Result<IOResult<Arc<Database>>> {
1375        loop {
1376            tracing::debug!(
1377                "open_with_flags_bypass_registry_async: state.phase={:?}",
1378                state.phase
1379            );
1380            match &state.phase {
1381                OpenDbAsyncPhase::Init => {
1382                    // Parse encryption key from encryption_opts if provided
1383                    let encryption_key = if let Some(ref enc_opts) = encryption_opts {
1384                        Some(EncryptionKey::from_hex_string(&enc_opts.hexkey)?)
1385                    } else {
1386                        None
1387                    };
1388
1389                    let wal_path = if let Some(wal_path) = wal_path {
1390                        wal_path
1391                    } else {
1392                        &format!("{path}-wal")
1393                    };
1394                    let mut db = Self::new(
1395                        opts,
1396                        flags,
1397                        path,
1398                        wal_path,
1399                        &io,
1400                        db_file.clone(),
1401                        encryption_opts.clone(),
1402                        allocator.clone(),
1403                    )?;
1404                    db.durable_storage.clone_from(&durable_storage);
1405
1406                    // Header validation + WAL recovery runs as a sub state
1407                    // machine in the ValidatingHeader phase so it can yield
1408                    // through IO instead of blocking. Stash the owned db and
1409                    // the parsed key for that phase.
1410                    state.building_db = Some(db);
1411                    state.encryption_key = encryption_key;
1412                    state.header_validation_state = HeaderValidationState::default();
1413                    state.phase = OpenDbAsyncPhase::ValidatingHeader;
1414                }
1415
1416                OpenDbAsyncPhase::ValidatingHeader => {
1417                    let db = state
1418                        .building_db
1419                        .as_mut()
1420                        .expect("building_db must be set in Init phase");
1421                    let mut hv_state = std::mem::take(&mut state.header_validation_state);
1422                    let result = db.header_validation(&mut hv_state, state.encryption_key.as_ref());
1423                    state.header_validation_state = hv_state;
1424                    let pager = return_if_io!(result);
1425
1426                    let mut db = state
1427                        .building_db
1428                        .take()
1429                        .expect("building_db must be set in Init phase");
1430
1431                    #[cfg(debug_assertions)]
1432                    {
1433                        let wal_enabled =
1434                            db.shared_wal.read().metadata.enabled.load(Ordering::SeqCst);
1435                        let mv_store_enabled = db.get_mv_store().is_some();
1436                        assert!(
1437                            db.is_readonly() || wal_enabled || mv_store_enabled,
1438                            "Either WAL or MVStore must be enabled"
1439                        );
1440                    }
1441                    let _ = &mut db;
1442
1443                    // Wrap db in Arc before connecting
1444                    let db = Arc::new(db);
1445
1446                    // Check: https://github.com/tursodatabase/turso/pull/1761#discussion_r2154013123
1447                    let conn =
1448                        db._connect(false, Some(pager.clone()), state.encryption_key.clone())?;
1449
1450                    // Acquire schema lock and hold it through ReadingHeader and LoadingSchema phases
1451                    // to ensure schema_version and make_from_btree are atomic
1452                    let guard = db.schema.lock_arc();
1453
1454                    state.db = Some(db);
1455                    state.pager = Some(pager);
1456                    state.conn = Some(conn);
1457                    state.schema_guard = Some(guard);
1458
1459                    state.phase = OpenDbAsyncPhase::ReadingHeader;
1460                }
1461
1462                OpenDbAsyncPhase::ReadingHeader => {
1463                    let pager = state
1464                        .pager
1465                        .as_ref()
1466                        .expect("pager must be initialized in Init phase");
1467                    let header_schema_cookie =
1468                        return_if_io!(pager.with_header(|header| header.schema_cookie.get()));
1469                    let guard = state
1470                        .schema_guard
1471                        .as_mut()
1472                        .expect("schema_guard must be acquired in Init phase");
1473                    // We logically exclusively own schema via the Opening sentinel in the
1474                    // registry which prevents concurrent opens of the same path.
1475                    // At this point we already created a connection which cloned the schema
1476                    // internally, so we can't use get_mut here.
1477                    //
1478                    // it's not ideal but correctness is OK - before prepare connection call maybe_update_schema and in case of divergence update schema ref from the db + we always check connection cookie in the VDBE program itself
1479                    let schema = Schema::try_make_mut(guard)?;
1480                    schema.schema_version = header_schema_cookie;
1481
1482                    state.phase = OpenDbAsyncPhase::LoadingSchema;
1483                }
1484
1485                OpenDbAsyncPhase::LoadingSchema => {
1486                    let pager = state
1487                        .pager
1488                        .as_ref()
1489                        .expect("pager must be initialized in Init phase");
1490                    let conn = state
1491                        .conn
1492                        .as_ref()
1493                        .expect("conn must be initialized in Init phase");
1494                    let syms = conn.syms.read();
1495
1496                    let guard = state
1497                        .schema_guard
1498                        .as_mut()
1499                        .expect("schema_guard must be acquired in Init phase");
1500                    // while we logically exclusively own schema as we hold DATABASE_MANAGER lock in the top level `open_with_flags_async_internal` function
1501                    // at the moment we already created connection which cloned the schema internally
1502                    // so, we can't use get_mut here for now
1503                    //
1504                    // it's not ideal but correctness is OK - before prepare connection call maybe_update_schema and in case of divergence update schema ref from the db + we always check connection cookie in the VDBE program itself
1505                    let schema = Schema::try_make_mut(guard)?;
1506
1507                    let result = schema.make_from_btree(
1508                        &mut state.make_from_btree_state,
1509                        None,
1510                        pager,
1511                        &syms,
1512                    );
1513
1514                    match result {
1515                        Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
1516                        Ok(IOResult::Done(())) => {
1517                            // Release the schema lock
1518                            state.schema_guard = None;
1519                        }
1520                        Err(LimboError::ExtensionError(e)) => {
1521                            // this means that a vtab exists and we no longer have the module loaded.
1522                            // we print a warning to the user to load the module
1523                            state.schema_guard = None;
1524                            tracing::warn!("open warning, failed to load extension: {e}");
1525                        }
1526                        Err(e) => return Err(e),
1527                    }
1528
1529                    // Load custom types from __turso_internal_types if the table
1530                    // exists and custom types are enabled. The schema loaded by
1531                    // make_from_btree includes the table definition but not its
1532                    // contents. We need to read the stored type definitions so
1533                    // that DECODE/ENCODE and affinity metadata are available to
1534                    // all subsequent connections.
1535                    if opts.enable_custom_types {
1536                        let conn = state
1537                            .conn
1538                            .as_ref()
1539                            .expect("conn must be initialized in Init phase");
1540                        // Sync the connection's schema from the database so it
1541                        // can query __turso_internal_types.
1542                        conn.maybe_update_schema();
1543                        let load_result: Result<()> = (|| {
1544                            let type_sqls = conn.query_stored_type_definitions()?;
1545                            if !type_sqls.is_empty() {
1546                                let db = state
1547                                    .db
1548                                    .as_ref()
1549                                    .expect("db must be initialized in Init phase");
1550                                db.with_schema_mut(|schema| {
1551                                    schema.load_type_definitions(&type_sqls)
1552                                })?;
1553                            }
1554                            Ok(())
1555                        })();
1556                        if let Err(e) = load_result {
1557                            tracing::warn!("Failed to load custom types during open: {}", e);
1558                        }
1559                    }
1560
1561                    state.phase = OpenDbAsyncPhase::BootstrapMvStore;
1562                }
1563
1564                OpenDbAsyncPhase::BootstrapMvStore => {
1565                    let db = state
1566                        .db
1567                        .as_ref()
1568                        .expect("db must be initialized in Init phase");
1569                    let pager = state
1570                        .pager
1571                        .as_ref()
1572                        .expect("pager must be initialized in Init phase");
1573
1574                    if let Some(mv_store) = db.get_mv_store().as_ref() {
1575                        // Create the dedicated bootstrap connection once and
1576                        // hold it across yields. Re-entry reuses the existing
1577                        // connection and the persisted `BootstrapState`.
1578                        if state.mvcc_bootstrap_conn.is_none() {
1579                            state.mvcc_bootstrap_conn = Some(db._connect(
1580                                true,
1581                                Some(pager.clone()),
1582                                state.encryption_key.clone(),
1583                            )?);
1584                        }
1585                        let conn = state.mvcc_bootstrap_conn.as_ref().expect("created above");
1586                        return_if_io!(
1587                            mv_store.bootstrap_nonblock(conn, &mut state.mvcc_bootstrap_state)
1588                        );
1589                        // Done — drop the bootstrap connection.
1590                        state.mvcc_bootstrap_conn = None;
1591                    }
1592
1593                    state.phase = OpenDbAsyncPhase::Done;
1594                    return Ok(IOResult::Done(
1595                        state
1596                            .db
1597                            .take()
1598                            .expect("db must be initialized in Init phase"),
1599                    ));
1600                }
1601
1602                OpenDbAsyncPhase::Done => {
1603                    panic!("open_with_flags_bypass_registry_async called after completion");
1604                }
1605            }
1606        }
1607    }
1608
1609    /// Necessary Pager initialization, so that we are prepared to read from Page 1.
1610    /// For encrypted databases, the encryption key must be provided to properly decrypt page 1.
1611    /// Blocking shim over [`Database::_init_nonblock`], retained for the
1612    /// synchronous callers (connection setup paths). The open state machine
1613    /// uses `_init_nonblock` directly so a fresh open never blocks here.
1614    pub(crate) fn _init(&self, encryption_key: Option<&EncryptionKey>) -> Result<Pager> {
1615        let mut st = InitState::default();
1616        self.io
1617            .block(|| self._init_nonblock(&mut st, encryption_key))
1618    }
1619
1620    /// Necessary Pager initialization, so that we are prepared to read from
1621    /// Page 1. For encrypted databases, the encryption key must be provided to
1622    /// properly decrypt page 1. Non-blocking: drives `init_pager` (DB-header
1623    /// read) and the page-1 autovacuum read through their IO.
1624    pub(crate) fn _init_nonblock(
1625        &self,
1626        st: &mut InitState,
1627        encryption_key: Option<&EncryptionKey>,
1628    ) -> Result<IOResult<Pager>> {
1629        loop {
1630            match st {
1631                InitState::Start => {
1632                    *st = InitState::InitPager(DbHeaderReadState::default());
1633                }
1634                InitState::InitPager(hdr_st) => {
1635                    let pager = return_if_io!(self.init_pager(None, hdr_st));
1636                    pager.enable_encryption(self.opts.enable_encryption);
1637
1638                    // Set up encryption context BEFORE reading the header page.
1639                    // For encrypted databases, page 1 has:
1640                    // - Bytes 0-15: Turso magic header (replaces SQLite magic)
1641                    // - Bytes 16-100: Unencrypted header metadata
1642                    // - Bytes 100+: Encrypted content
1643                    // The encryption context is needed to properly decrypt page 1 when reopening.
1644                    if let Some(key) = encryption_key {
1645                        let cipher_mode = self.encryption_cipher_mode.get();
1646                        pager.set_encryption_context(cipher_mode, key)?;
1647                    }
1648
1649                    // Start a read transaction before reading page 1 to prevent a concurrent
1650                    // checkpoint from truncating the WAL underneath bootstrap. Under heavy
1651                    // same-process connection churn, the shared WAL bootstrap path can
1652                    // briefly contend on short-lived in-process locks, so treat Busy here as
1653                    // a transient and retry rather than failing `connect()`.
1654                    let mut read_tx_attempts = 0u32;
1655                    loop {
1656                        match pager.begin_read_tx() {
1657                            Ok(()) => break,
1658                            Err(LimboError::Busy) => {
1659                                read_tx_attempts += 1;
1660                                if read_tx_attempts > 1 {
1661                                    return Err(LimboError::Busy);
1662                                }
1663                                pager.io.yield_now();
1664                            }
1665                            Err(err) => return Err(err),
1666                        }
1667                    }
1668
1669                    *st = InitState::ReadPage1 {
1670                        pager: Box::new(pager),
1671                    };
1672                }
1673                InitState::ReadPage1 { pager } => {
1674                    // Read page 1 within the read transaction to determine the
1675                    // autovacuum mode. The read tx stays open across an IO
1676                    // yield here (re-entry resumes the read); we only end it
1677                    // once the read completes or errors.
1678                    let mode = match HeaderRef::from_pager(pager) {
1679                        Ok(IOResult::Done(header_ref)) => {
1680                            let header = header_ref.borrow();
1681                            if header.vacuum_mode_largest_root_page.get() > 0 {
1682                                if header.incremental_vacuum_enabled.get() > 0 {
1683                                    AutoVacuumMode::Incremental
1684                                } else {
1685                                    AutoVacuumMode::Full
1686                                }
1687                            } else {
1688                                AutoVacuumMode::None
1689                            }
1690                        }
1691                        Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
1692                        Err(err) => {
1693                            pager.end_read_tx();
1694                            return Err(err);
1695                        }
1696                    };
1697
1698                    pager.end_read_tx();
1699                    pager.set_auto_vacuum_mode(mode);
1700
1701                    let InitState::ReadPage1 { pager } = std::mem::take(st) else {
1702                        unreachable!("state is ReadPage1");
1703                    };
1704                    return Ok(IOResult::Done(*pager));
1705                }
1706            }
1707        }
1708    }
1709
1710    /// Checks the Version numbers in the DatabaseHeader, and changes it according to the required options
1711    ///
1712    /// Will also open MVStore and WAL if needed.
1713    ///
1714    /// Driven as a sub state machine (see [`HeaderValidationState`]) from the
1715    /// `ValidatingHeader` open phase so that WAL recovery on open yields
1716    /// through its IO instead of blocking — this is what lets a fresh open
1717    /// make progress on runtimes (e.g. WASM) that cannot pump `io.step`
1718    /// synchronously.
1719    fn header_validation(
1720        &mut self,
1721        st: &mut HeaderValidationState,
1722        encryption_key: Option<&EncryptionKey>,
1723    ) -> Result<IOResult<Arc<Pager>>> {
1724        loop {
1725            match st {
1726                HeaderValidationState::Start { init } => {
1727                    // `_init` does not modify `open_flags` (the autovacuum
1728                    // override happens later in `Validate`), so capturing
1729                    // `is_readonly` across the `_init` yields is stable.
1730                    let pager = return_if_io!(self._init_nonblock(init, encryption_key));
1731                    let log_exists =
1732                        journal_mode::logical_log_exists(std::path::Path::new(&self.path));
1733                    let is_readonly = self.open_flags.contains(OpenFlags::ReadOnly);
1734                    turso_assert!(pager.wal.is_none(), "Pager should have no WAL yet");
1735                    *st = HeaderValidationState::Validate {
1736                        pager: Box::new(pager),
1737                        is_readonly,
1738                        log_exists,
1739                    };
1740                }
1741                HeaderValidationState::Validate {
1742                    pager,
1743                    is_readonly,
1744                    log_exists,
1745                } => {
1746                    let is_readonly = *is_readonly;
1747                    let log_exists = *log_exists;
1748
1749                    // Re-entrant reads: both `with_header` and `from_pager`
1750                    // resume via their own state machines, and the autovacuum
1751                    // flag update is idempotent.
1752                    let is_autovacuumed_db = return_if_io!(pager.with_header(|header| {
1753                        header.vacuum_mode_largest_root_page.get() > 0
1754                            || header.incremental_vacuum_enabled.get() > 0
1755                    }));
1756                    if is_autovacuumed_db && !self.opts.enable_autovacuum {
1757                        tracing::warn!(
1758                            "Database has autovacuum enabled but --experimental-autovacuum flag is not set. Opening in readonly mode."
1759                        );
1760                        self.open_flags |= OpenFlags::ReadOnly;
1761                    }
1762
1763                    let header: HeaderRefMut = return_if_io!(HeaderRefMut::from_pager(pager));
1764                    let header_mut = header.borrow_mut();
1765
1766                    if !header_mut.text_encoding.is_utf8() {
1767                        return Err(LimboError::UnsupportedEncoding(
1768                            header_mut.text_encoding.to_string(),
1769                        ));
1770                    }
1771
1772                    let (read_version, write_version) =
1773                        { (header_mut.read_version, header_mut.write_version) };
1774
1775                    if encryption_key.is_none() && header_mut.magic != SQLITE_HEADER {
1776                        tracing::error!(
1777                            "invalid value of database header magic bytes: {:?}",
1778                            header_mut.magic
1779                        );
1780                        return Err(LimboError::NotADB);
1781                    }
1782                    // when we open fresh db with encryption params - header will be SQLite at this point
1783                    if encryption_key.is_some()
1784                        && (header_mut.magic != SQLITE_HEADER
1785                            && !header_mut.magic.starts_with(TURSO_HEADER_PREFIX))
1786                    {
1787                        tracing::error!(
1788                            "invalid value of database header magic bytes: {:?}",
1789                            header_mut.magic
1790                        );
1791                        return Err(LimboError::NotADB);
1792                    }
1793
1794                    // TODO: right now we don't support READ ONLY and no READ or WRITE in the Version header
1795                    // https://www.sqlite.org/fileformat.html#file_format_version_numbers
1796                    if read_version != write_version {
1797                        return Err(LimboError::Corrupt(format!(
1798                            "Read version `{read_version:?}` is not equal to Write version `{write_version:?} in database header`"
1799                        )));
1800                    }
1801
1802                    let (read_version, _write_version) = (
1803                        read_version.to_version().map_err(|val| {
1804                            LimboError::Corrupt(format!("Invalid read_version: {val}"))
1805                        })?,
1806                        write_version.to_version().map_err(|val| {
1807                            LimboError::Corrupt(format!("Invalid write_version: {val}"))
1808                        })?,
1809                    );
1810
1811                    // Validate fixed header fields per SQLite spec
1812                    if header_mut.max_embed_frac != 64 {
1813                        return Err(LimboError::Corrupt(format!(
1814                            "Invalid max_embed_frac: expected 64, got {}",
1815                            header_mut.max_embed_frac
1816                        )));
1817                    }
1818                    if header_mut.min_embed_frac != 32 {
1819                        return Err(LimboError::Corrupt(format!(
1820                            "Invalid min_embed_frac: expected 32, got {}",
1821                            header_mut.min_embed_frac
1822                        )));
1823                    }
1824                    if header_mut.leaf_frac != 32 {
1825                        return Err(LimboError::Corrupt(format!(
1826                            "Invalid leaf_frac: expected 32, got {}",
1827                            header_mut.leaf_frac
1828                        )));
1829                    }
1830                    let schema_format = header_mut.schema_format.get();
1831                    // If the database is completely empty, if it has no schema, then the schema format number can be zero.
1832                    if !(0..=4).contains(&schema_format) {
1833                        return Err(LimboError::Corrupt(format!(
1834                            "Invalid schema_format: expected 1-4, got {schema_format}"
1835                        )));
1836                    }
1837                    if !matches!(
1838                        header_mut.text_encoding,
1839                        TextEncoding::Unset
1840                            | TextEncoding::Utf8
1841                            | TextEncoding::Utf16Le
1842                            | TextEncoding::Utf16Be
1843                    ) {
1844                        return Err(LimboError::Corrupt(format!(
1845                            "Invalid text_encoding: {}",
1846                            header_mut.text_encoding
1847                        )));
1848                    }
1849                    if !matches!(
1850                        header_mut.text_encoding,
1851                        TextEncoding::Unset | TextEncoding::Utf8
1852                    ) {
1853                        return Err(LimboError::Corrupt(format!(
1854                            "Only utf8 text_encoding is supported by tursodb: got={}",
1855                            header_mut.text_encoding
1856                        )));
1857                    }
1858
1859                    // Determine if we should open in MVCC mode based on the database header version
1860                    // MVCC is controlled only by the database header (set via PRAGMA journal_mode)
1861                    let open_mv_store = matches!(read_version, Version::Mvcc);
1862
1863                    // MVCC has no cross-process coordination: commit
1864                    // serialization, the logical-log append offset, and
1865                    // checkpoint exclusion are all process-local, so
1866                    // concurrent multiprocess access silently loses committed
1867                    // transactions and corrupts live views.
1868                    if open_mv_store && self.opts.enable_multiprocess_wal {
1869                        return Err(LimboError::InvalidArgument(format!(
1870                            "cannot open MVCC database '{}' with experimental multiprocess WAL: MVCC does not support multiprocess access",
1871                            self.path
1872                        )));
1873                    }
1874
1875                    // Now check the Header Version to see which mode the DB file really is on
1876                    // Track if header was modified so we can write it to disk
1877                    let header_modified = match read_version {
1878                        Version::Legacy => {
1879                            if is_readonly {
1880                                tracing::warn!(
1881                                    "Database {} is opened in readonly mode, cannot convert Legacy mode to WAL. Running in Legacy mode.",
1882                                    self.path
1883                                );
1884                                false
1885                            } else {
1886                                // Convert Legacy to WAL mode
1887                                header_mut.read_version = RawVersion::from(Version::Wal);
1888                                header_mut.write_version = RawVersion::from(Version::Wal);
1889                                true
1890                            }
1891                        }
1892                        Version::Wal => false,
1893                        Version::Mvcc => false,
1894                    };
1895
1896                    // In WAL mode, a logical log is always unexpected.
1897                    // In MVCC mode, WAL and logical-log coexistence can happen across interrupted checkpoint
1898                    // recovery and is reconciled in MvStore::bootstrap().
1899                    if !open_mv_store && log_exists {
1900                        return Err(LimboError::Corrupt(format!(
1901                            "MVCC logical log file exists for database {}, but database header indicates WAL mode. The database may be corrupted.",
1902                            self.path
1903                        )));
1904                    }
1905
1906                    let page = header.page().clone();
1907                    // `header` (a cheap Arc<Page> wrapper, no lock) is dropped
1908                    // here; the page ref carries the (possibly modified) header
1909                    // buffer forward.
1910                    drop(header);
1911
1912                    // Move the owned pager out of the state to build the next.
1913                    let HeaderValidationState::Validate { pager, .. } = std::mem::take(st) else {
1914                        unreachable!("state is Validate");
1915                    };
1916                    *st = if header_modified {
1917                        HeaderValidationState::WriteHeader {
1918                            pager,
1919                            page,
1920                            open_mv_store,
1921                            completion: None,
1922                        }
1923                    } else {
1924                        HeaderValidationState::OpenWal {
1925                            pager,
1926                            open_mv_store,
1927                            driver: None,
1928                        }
1929                    };
1930                }
1931                HeaderValidationState::WriteHeader {
1932                    pager,
1933                    page,
1934                    open_mv_store,
1935                    completion,
1936                } => {
1937                    // If header was modified, write it directly to disk before we attach the
1938                    // WAL / clear the cache (must hit the DB file, not the WAL).
1939                    let c = match completion.take() {
1940                        Some(c) => c,
1941                        None => storage::sqlite3_ondisk::begin_write_btree_page(pager, page)?,
1942                    };
1943                    if !c.succeeded() {
1944                        *completion = Some(c.clone());
1945                        io_yield_one!(c);
1946                    }
1947                    let open_mv_store = *open_mv_store;
1948                    let HeaderValidationState::WriteHeader { pager, .. } = std::mem::take(st)
1949                    else {
1950                        unreachable!("state is WriteHeader");
1951                    };
1952                    *st = HeaderValidationState::OpenWal {
1953                        pager,
1954                        open_mv_store,
1955                        driver: None,
1956                    };
1957                }
1958                HeaderValidationState::OpenWal {
1959                    open_mv_store,
1960                    driver,
1961                    ..
1962                } => {
1963                    // Always open shared WAL and set it in the Database and Pager.
1964                    // MVCC currently requires a WAL open to function.
1965                    let shared_wal = {
1966                        #[cfg(not(host_shared_wal))]
1967                        {
1968                            if driver.is_none() {
1969                                *driver = Some(WalFileShared::open_shared_if_exists_begin(
1970                                    &self.io,
1971                                    &self.wal_path,
1972                                    self.open_flags,
1973                                )?);
1974                            }
1975                            return_if_io!(driver.as_mut().expect("driver initialized above").poll())
1976                        }
1977                        #[cfg(host_shared_wal)]
1978                        {
1979                            // Native-only coordination path: `io.step` pumps
1980                            // synchronously here, so the blocking shims are
1981                            // fine. (Driver field is unused on host.)
1982                            let _ = &driver;
1983                            let flags = self.open_flags;
1984                            let shared_authority = self.open_shared_wal_coordination_for_open()?;
1985                            if let Some(authority) = shared_authority.as_ref() {
1986                                if !authority.frame_index_overflowed() {
1987                                    WalFileShared::open_shared_from_authority_if_exists(
1988                                        &self.io,
1989                                        &self.wal_path,
1990                                        flags,
1991                                        authority,
1992                                        &self.db_file,
1993                                    )?
1994                                } else {
1995                                    WalFileShared::open_shared_if_exists(
1996                                        &self.io,
1997                                        &self.wal_path,
1998                                        flags,
1999                                    )?
2000                                }
2001                            } else {
2002                                WalFileShared::open_shared_if_exists(
2003                                    &self.io,
2004                                    &self.wal_path,
2005                                    flags,
2006                                )?
2007                            }
2008                        }
2009                    };
2010
2011                    let open_mv_store = *open_mv_store;
2012                    let HeaderValidationState::OpenWal { mut pager, .. } = std::mem::take(st)
2013                    else {
2014                        unreachable!("state is OpenWal");
2015                    };
2016
2017                    self.shared_wal = shared_wal;
2018                    let last_checksum_and_max_frame =
2019                        self.shared_wal.read().last_checksum_and_max_frame();
2020                    let wal =
2021                        self.build_wal(last_checksum_and_max_frame, pager.buffer_pool.clone())?;
2022                    pager.set_wal(wal);
2023
2024                    // Clear page cache after attaching WAL since pages may have been cached
2025                    // from disk reads before WAL was attached. The WAL may contain newer
2026                    // versions of these pages (e.g., page 1 with updated schema_cookie).
2027                    pager.clear_page_cache(true);
2028                    pager.set_schema_cookie(None);
2029
2030                    if open_mv_store {
2031                        let canonical_path = self.get_database_canonical_path();
2032                        let enc_ctx = pager.io_ctx.read().encryption_context().cloned();
2033                        let mv_store = journal_mode::open_mv_store(
2034                            self.io.clone(),
2035                            &canonical_path,
2036                            self.open_flags,
2037                            self.durable_storage.clone(),
2038                            enc_ctx,
2039                            self.mv_store_allocator.clone(),
2040                            self.experimental_mvcc_passive_checkpoint_enabled(),
2041                        )?;
2042                        self.mv_store.store(Some(mv_store));
2043                    }
2044
2045                    return Ok(IOResult::Done(Arc::new(*pager)));
2046                }
2047            }
2048        }
2049    }
2050
2051    pub fn get_database_canonical_path(&self) -> String {
2052        if self.is_in_memory_db() {
2053            // For in-memory databases, SQLite shows empty string
2054            String::new()
2055        } else {
2056            // For file databases, try show the full absolute path if that doesn't fail
2057            match std::fs::canonicalize(&self.path) {
2058                Ok(abs_path) => abs_path.to_string_lossy().to_string(),
2059                Err(_) => self.path.to_string(),
2060            }
2061        }
2062    }
2063
2064    #[cfg(clt_turso_feature = "conn_raw_api")]
2065    /// Rebuild the process-local shared WAL view after a caller restores the
2066    /// database and WAL files outside the pager.
2067    pub fn reload_wal_after_external_restore(self: &Arc<Self>) -> Result<()> {
2068        let flags = self.open_flags;
2069        #[cfg(host_shared_wal)]
2070        let shared_authority = self.open_shared_wal_coordination_for_open()?;
2071        #[cfg(not(host_shared_wal))]
2072        let shared_authority: Option<()> = None;
2073
2074        let new_shared_wal = {
2075            #[cfg(host_shared_wal)]
2076            {
2077                if let Some(authority) = shared_authority.as_ref() {
2078                    if !authority.frame_index_overflowed() {
2079                        WalFileShared::open_shared_from_authority_if_exists(
2080                            &self.io,
2081                            &self.wal_path,
2082                            flags,
2083                            authority,
2084                            &self.db_file,
2085                        )?
2086                    } else {
2087                        WalFileShared::open_shared_if_exists(&self.io, &self.wal_path, flags)?
2088                    }
2089                } else {
2090                    WalFileShared::open_shared_if_exists(&self.io, &self.wal_path, flags)?
2091                }
2092            }
2093            #[cfg(not(host_shared_wal))]
2094            {
2095                WalFileShared::open_shared_if_exists(&self.io, &self.wal_path, flags)?
2096            }
2097        };
2098        let new_shared_wal = Arc::try_unwrap(new_shared_wal).map_err(|_| {
2099            LimboError::InternalError(
2100                "new WAL state unexpectedly shared during external restore reload".to_string(),
2101            )
2102        })?;
2103        self.shared_wal
2104            .write()
2105            .replace_after_external_restore(new_shared_wal.into_inner());
2106        if self.mvcc_enabled() || journal_mode::logical_log_exists(std::path::Path::new(&self.path))
2107        {
2108            let mv_store = journal_mode::open_mv_store(
2109                self.io.clone(),
2110                &self.path,
2111                self.open_flags,
2112                self.durable_storage.clone(),
2113                None,
2114                self.mv_store_allocator.clone(),
2115                self.experimental_mvcc_passive_checkpoint_enabled(),
2116            )?;
2117            self.mv_store.store(Some(mv_store.clone()));
2118            let mvcc_bootstrap_conn = self._connect(true, None, None)?;
2119            match mv_store.bootstrap(mvcc_bootstrap_conn.clone()) {
2120                Ok(()) => {}
2121                Err(LimboError::SchemaUpdated) => {
2122                    mvcc_bootstrap_conn.force_reparse_schema()?;
2123                    mv_store.bootstrap(mvcc_bootstrap_conn)?;
2124                }
2125                Err(error) => return Err(error),
2126            }
2127        } else {
2128            self.mv_store.store(None);
2129        }
2130        Ok(())
2131    }
2132
2133    #[instrument(skip_all, level = Level::DEBUG)]
2134    pub fn connect(self: &Arc<Database>) -> Result<Arc<Connection>> {
2135        self._connect(false, None, None)
2136    }
2137
2138    /// Connect with an encryption key.
2139    /// Use this when opening an encrypted database where the key is known at connect time.
2140    #[instrument(skip_all, level = Level::DEBUG)]
2141    pub fn connect_with_encryption(
2142        self: &Arc<Database>,
2143        encryption_key: Option<EncryptionKey>,
2144    ) -> Result<Arc<Connection>> {
2145        self._connect(false, None, encryption_key)
2146    }
2147
2148    #[instrument(skip_all, level = Level::DEBUG)]
2149    fn _connect(
2150        self: &Arc<Database>,
2151        is_mvcc_bootstrap_connection: bool,
2152        pager: Option<Arc<Pager>>,
2153        encryption_key: Option<EncryptionKey>,
2154    ) -> Result<Arc<Connection>> {
2155        let pager = if let Some(pager) = pager {
2156            pager
2157        } else {
2158            // Pass encryption key to _init so it can set up encryption context
2159            // before reading page 1. This is required for reopening encrypted databases.
2160            Arc::new(self._init(encryption_key.as_ref())?)
2161        };
2162        let default_cache_size = pager
2163            .io
2164            .block(|| pager.with_header(|header| header.default_page_cache_size))
2165            .unwrap_or_default()
2166            .get();
2167
2168        self._connect_with_pager_and_default_cache_size(
2169            is_mvcc_bootstrap_connection,
2170            pager,
2171            encryption_key,
2172            default_cache_size,
2173        )
2174    }
2175
2176    pub(crate) fn _connect_with_pager_and_default_cache_size(
2177        self: &Arc<Database>,
2178        is_mvcc_bootstrap_connection: bool,
2179        pager: Arc<Pager>,
2180        encryption_key: Option<EncryptionKey>,
2181        default_cache_size: i32,
2182    ) -> Result<Arc<Connection>> {
2183        let page_size = pager.get_page_size_unchecked();
2184        let encryption_cipher = self.encryption_cipher_mode.get();
2185        let conn = Arc::new(Connection {
2186            db: self.clone(),
2187            pager: ArcSwap::new(pager),
2188            schema: RwLock::new(self.schema.lock().clone()),
2189            database_schemas: RwLock::new(HashMap::default()),
2190            auto_commit: AtomicBool::new(true),
2191            transaction_state: AtomicTransactionState::new(TransactionState::None),
2192            poisoned_tx: AtomicBool::new(false),
2193            last_insert_rowid: AtomicI64::new(0),
2194            changes: AtomicI64::new(0),
2195            total_changes: AtomicI64::new(0),
2196            syms: parking_lot::RwLock::new(SymbolTable::new()),
2197            _shared_cache: false,
2198            cache_size: AtomicI32::new(default_cache_size),
2199            page_size: AtomicU16::new(page_size.get_raw()),
2200            wal_auto_actions: AtomicU8::new(WalAutoActions::all_enabled().bits()),
2201            #[cfg(clt_turso_feature = "conn_raw_api")]
2202            portable_logical_changes_enabled: AtomicBool::new(false),
2203            #[cfg(clt_turso_feature = "conn_raw_api")]
2204            mvcc_log_metadata: RwLock::new(HashMap::default()),
2205            capture_data_changes: RwLock::new(None),
2206            cdc_transaction_id: AtomicI64::new(-1),
2207            closed: AtomicBool::new(false),
2208            temp: crate::connection::TempDbContext::new(),
2209            attached_databases: RwLock::new(DatabaseCatalog::new()),
2210            query_only: AtomicBool::new(false),
2211            vdbe_trace: AtomicBool::new(false),
2212            dml_require_where: AtomicBool::new(false),
2213            dqs_dml: AtomicBool::new(true),
2214            sequence_inner_retries: AtomicU64::new(0),
2215            mv_tx: RwLock::new(None),
2216            attached_mv_txs: RwLock::new(HashMap::default()),
2217            #[cfg(any(clt_turso_tests, injected_yields))]
2218            yield_injector: RwLock::new(None),
2219            #[cfg(any(clt_turso_tests, injected_yields))]
2220            failure_injector: RwLock::new(None),
2221            #[cfg(any(clt_turso_tests, injected_yields))]
2222            yield_instance_id_counter: AtomicU64::new(1),
2223            view_transaction_states: AllViewsTxState::new(),
2224            metrics: RwLock::new(ConnectionMetrics::new()),
2225            nestedness: AtomicI32::new(0),
2226            compiling_triggers: RwLock::new(Vec::new()),
2227            executing_triggers: RwLock::new(Vec::new()),
2228            encryption_key: RwLock::new(encryption_key),
2229            encryption_cipher_mode: AtomicCipherMode::new(encryption_cipher),
2230            sync_mode: AtomicSyncMode::new(SyncMode::Full),
2231            temp_store: AtomicTempStore::new(TempStore::Default),
2232            data_sync_retry: AtomicBool::new(false),
2233            busy_handler: RwLock::new(BusyHandler::None),
2234            progress_handler: ProgressHandler::new(),
2235            query_timeout_ms: AtomicU64::new(0),
2236            interrupt_requested: AtomicBool::new(false),
2237            is_mvcc_bootstrap_connection: AtomicBool::new(is_mvcc_bootstrap_connection),
2238            full_column_names: AtomicBool::new(false),
2239            short_column_names: AtomicBool::new(true),
2240            enable_load_extension: AtomicBool::new(self.can_load_extensions()),
2241            fk_pragma: AtomicBool::new(false),
2242            fk_deferred_violations: AtomicIsize::new(0),
2243            n_active_writes: AtomicI32::new(0),
2244            n_active_root_statements: AtomicI32::new(0),
2245            check_constraints_pragma: AtomicBool::new(false),
2246            vtab_txn_states: RwLock::new(HashSet::default()),
2247            named_savepoints: RwLock::new(Vec::new()),
2248            schema_reparse_in_progress: AtomicBool::new(false),
2249            prepare_context_generation: AtomicU64::new(0),
2250            sequence_currvals: RwLock::new(HashMap::default()),
2251        });
2252        self.n_connections
2253            .fetch_add(1, crate::sync::atomic::Ordering::SeqCst);
2254        let builtin_syms = self.builtin_syms.read();
2255        // add built-in extensions symbols to the connection to prevent having to load each time
2256        conn.syms.write().extend(&builtin_syms);
2257        refresh_analyze_stats(&conn);
2258        Ok(conn)
2259    }
2260
2261    pub fn is_readonly(&self) -> bool {
2262        self.open_flags.contains(OpenFlags::ReadOnly)
2263    }
2264
2265    /// If we do not have a physical WAL file, but we know the database file is initialized on disk,
2266    /// we need to read the page_size from the database header.
2267    /// Non-blocking read of the 512-byte database file header (page 1's
2268    /// header region). Yields the read completion via the supplied state until
2269    /// it finishes, then returns the filled buffer.
2270    fn read_db_header_buf(&self, st: &mut DbHeaderReadState) -> Result<IOResult<Arc<Buffer>>> {
2271        loop {
2272            match st {
2273                DbHeaderReadState::Start => {
2274                    turso_assert!(
2275                        PageSize::MIN % 512 == 0,
2276                        "header read must be a multiple of 512 for O_DIRECT"
2277                    );
2278                    let buf = Arc::new(Buffer::new_temporary(PageSize::MIN as usize));
2279                    let c = new_header_read_completion(buf.clone());
2280                    let c = self.db_file.read_header(c)?;
2281                    *st = DbHeaderReadState::Reading { buf, completion: c };
2282                }
2283                DbHeaderReadState::Reading { buf, completion } => {
2284                    if !completion.succeeded() {
2285                        let c = completion.clone();
2286                        io_yield_one!(c);
2287                    }
2288                    return Ok(IOResult::Done(buf.clone()));
2289                }
2290            }
2291        }
2292    }
2293
2294    /// Determine the actual page size, in order of preference:
2295    /// 1. From the WAL header if it exists and is initialized
2296    /// 2. From `header_page_size` (read from the DB header by the caller) if
2297    ///    the database is initialized
2298    ///
2299    /// Otherwise, fall back to, in order of preference:
2300    /// 1. From the requested page size if it is provided
2301    /// 2. PageSize::default(), i.e. 4096
2302    fn determine_actual_page_size(
2303        &self,
2304        shared_wal: &WalFileShared,
2305        requested_page_size: Option<usize>,
2306        header_page_size: Option<PageSize>,
2307    ) -> Result<PageSize> {
2308        if shared_wal.metadata.enabled.load(Ordering::SeqCst) {
2309            let size_in_wal = shared_wal.page_size();
2310            if size_in_wal != 0 {
2311                let Some(page_size) = PageSize::new(size_in_wal) else {
2312                    bail_corrupt_error!("invalid page size in WAL: {size_in_wal}");
2313                };
2314                return Ok(page_size);
2315            }
2316        }
2317        if let Some(page_size) = header_page_size {
2318            Ok(page_size)
2319        } else {
2320            let Some(size) = requested_page_size else {
2321                return Ok(PageSize::default());
2322            };
2323            let Some(page_size) = PageSize::new(size as u32) else {
2324                bail_corrupt_error!("invalid requested page size: {size}");
2325            };
2326            Ok(page_size)
2327        }
2328    }
2329
2330    #[cfg(all(unix, target_pointer_width = "64", target_os = "macos"))]
2331    fn filesystem_type_allows_shared_wal(fs_type: &str) -> bool {
2332        // Network and distributed filesystems where mmap'd shared memory
2333        // cannot guarantee cross-process coherency.
2334        !matches!(
2335            fs_type,
2336            "nfs" | "smbfs" | "afpfs" | "webdav" | "cifs" | "acfs"
2337        )
2338    }
2339
2340    #[cfg(all(
2341        unix,
2342        target_pointer_width = "64",
2343        not(any(target_os = "linux", target_os = "android")),
2344        not(target_os = "macos")
2345    ))]
2346    fn filesystem_type_allows_shared_wal(_fs_type: &str) -> bool {
2347        true
2348    }
2349
2350    #[cfg(all(
2351        unix,
2352        target_pointer_width = "64",
2353        any(target_os = "linux", target_os = "android")
2354    ))]
2355    fn filesystem_magic_allows_shared_wal(filesystem_magic: libc::c_long) -> bool {
2356        const AFS_SUPER_MAGIC: libc::c_long = 0x5346_414f;
2357        const CIFS_SUPER_MAGIC: libc::c_long = 0xFF53_4D42u32 as libc::c_long;
2358        const CODA_SUPER_MAGIC: libc::c_long = 0x7375_7245;
2359        const CEPH_SUPER_MAGIC: libc::c_long = 0x00C3_6400;
2360        const GFS2_SUPER_MAGIC: libc::c_long = 0x0116_1970;
2361        const LUSTRE_SUPER_MAGIC: libc::c_long = 0x0BD0_0BD0;
2362        const NCP_SUPER_MAGIC: libc::c_long = 0x564c;
2363        const NFS_SUPER_MAGIC: libc::c_long = 0x6969;
2364        const OCFS2_SUPER_MAGIC: libc::c_long = 0x7461_636f;
2365        const SMB2_SUPER_MAGIC: libc::c_long = 0xFE53_4D42u32 as libc::c_long;
2366        const V9FS_SUPER_MAGIC: libc::c_long = 0x0102_1997;
2367
2368        !matches!(
2369            filesystem_magic,
2370            AFS_SUPER_MAGIC
2371                | CIFS_SUPER_MAGIC
2372                | CODA_SUPER_MAGIC
2373                | CEPH_SUPER_MAGIC
2374                | GFS2_SUPER_MAGIC
2375                | LUSTRE_SUPER_MAGIC
2376                | NCP_SUPER_MAGIC
2377                | NFS_SUPER_MAGIC
2378                | OCFS2_SUPER_MAGIC
2379                | SMB2_SUPER_MAGIC
2380                | V9FS_SUPER_MAGIC
2381        )
2382    }
2383
2384    #[cfg(all(
2385        unix,
2386        target_pointer_width = "64",
2387        any(target_os = "linux", target_os = "android")
2388    ))]
2389    fn path_allows_shared_wal_coordination(path: &Path) -> Result<bool> {
2390        use std::ffi::CString;
2391        use std::os::unix::ffi::OsStrExt;
2392
2393        let probe_path = if path.exists() {
2394            path
2395        } else {
2396            path.parent()
2397                .filter(|parent| !parent.as_os_str().is_empty())
2398                .unwrap_or_else(|| Path::new("."))
2399        };
2400        let c_path = CString::new(probe_path.as_os_str().as_bytes()).map_err(|_| {
2401            LimboError::InvalidArgument(format!(
2402                "path contains interior NUL bytes: {}",
2403                probe_path.display()
2404            ))
2405        })?;
2406        let mut stat = std::mem::MaybeUninit::<libc::statfs>::uninit();
2407        let rc = unsafe { libc::statfs(c_path.as_ptr(), stat.as_mut_ptr()) };
2408        if rc != 0 {
2409            return Err(io_error(
2410                std::io::Error::last_os_error(),
2411                "statfs shared WAL coordination path",
2412            ));
2413        }
2414        let stat = unsafe { stat.assume_init() };
2415        Ok(Self::filesystem_magic_allows_shared_wal(
2416            stat.f_type as libc::c_long,
2417        ))
2418    }
2419
2420    #[cfg(all(
2421        unix,
2422        target_pointer_width = "64",
2423        not(any(target_os = "linux", target_os = "android"))
2424    ))]
2425    fn path_allows_shared_wal_coordination(path: &Path) -> Result<bool> {
2426        use std::ffi::CString;
2427        use std::os::unix::ffi::OsStrExt;
2428
2429        let probe_path = if path.exists() {
2430            path
2431        } else {
2432            path.parent()
2433                .filter(|parent| !parent.as_os_str().is_empty())
2434                .unwrap_or_else(|| Path::new("."))
2435        };
2436        let c_path = CString::new(probe_path.as_os_str().as_bytes()).map_err(|_| {
2437            LimboError::InvalidArgument(format!(
2438                "path contains interior NUL bytes: {}",
2439                probe_path.display()
2440            ))
2441        })?;
2442        let mut stat = std::mem::MaybeUninit::<libc::statfs>::uninit();
2443        let rc = unsafe { libc::statfs(c_path.as_ptr(), stat.as_mut_ptr()) };
2444        if rc != 0 {
2445            return Err(io_error(
2446                std::io::Error::last_os_error(),
2447                "statfs shared WAL coordination path",
2448            ));
2449        }
2450        let stat = unsafe { stat.assume_init() };
2451        // macOS and other BSDs expose the filesystem type as a
2452        // null-terminated string in f_fstypename rather than an
2453        // integer magic number.
2454        let fs_type = unsafe {
2455            std::ffi::CStr::from_ptr(stat.f_fstypename.as_ptr())
2456                .to_str()
2457                .unwrap_or("")
2458        };
2459        Ok(Self::filesystem_type_allows_shared_wal(fs_type))
2460    }
2461
2462    #[cfg(all(target_os = "windows", target_pointer_width = "64"))]
2463    fn path_allows_shared_wal_coordination(path: &Path) -> Result<bool> {
2464        use std::iter::once;
2465        use std::os::windows::ffi::OsStrExt;
2466        use windows_sys::Win32::Storage::FileSystem::{GetDriveTypeW, GetVolumePathNameW};
2467
2468        const DRIVE_REMOVABLE: u32 = 2;
2469        const DRIVE_FIXED: u32 = 3;
2470        const DRIVE_REMOTE: u32 = 4;
2471        const DRIVE_RAMDISK: u32 = 6;
2472
2473        let probe_path = if path.exists() {
2474            path.to_path_buf()
2475        } else {
2476            path.parent()
2477                .filter(|parent| !parent.as_os_str().is_empty())
2478                .unwrap_or_else(|| Path::new("."))
2479                .to_path_buf()
2480        };
2481        let probe_path = if probe_path.is_absolute() {
2482            probe_path
2483        } else {
2484            std::env::current_dir()
2485                .map_err(|err| io_error(err, "resolve shared WAL coordination path"))?
2486                .join(probe_path)
2487        };
2488        let probe_path_wide: Vec<u16> = probe_path
2489            .as_os_str()
2490            .encode_wide()
2491            .chain(once(0))
2492            .collect();
2493        let mut volume_path = vec![0u16; 261];
2494        let result = unsafe {
2495            GetVolumePathNameW(
2496                probe_path_wide.as_ptr(),
2497                volume_path.as_mut_ptr(),
2498                volume_path.len() as u32,
2499            )
2500        };
2501        if result == 0 {
2502            return Err(io_error(
2503                std::io::Error::last_os_error(),
2504                "GetVolumePathNameW shared WAL coordination path",
2505            ));
2506        }
2507
2508        let drive_type = unsafe { GetDriveTypeW(volume_path.as_ptr()) };
2509        Ok(
2510            matches!(drive_type, DRIVE_FIXED | DRIVE_RAMDISK | DRIVE_REMOVABLE)
2511                && drive_type != DRIVE_REMOTE,
2512        )
2513    }
2514
2515    #[cfg(host_shared_wal)]
2516    pub(crate) fn shared_wal_coordination(
2517        &self,
2518    ) -> Result<Option<Arc<MappedSharedWalCoordination>>> {
2519        let shared_wal = self.shared_wal.read();
2520        if !shared_wal.metadata.enabled.load(Ordering::Acquire) {
2521            return Ok(None);
2522        }
2523        drop(shared_wal);
2524        self.open_shared_wal_coordination_inner()
2525    }
2526
2527    #[cfg(not(host_shared_wal))]
2528    pub(crate) fn shared_wal_coordination(&self) -> Result<Option<()>> {
2529        Ok(None)
2530    }
2531
2532    #[cfg(host_shared_wal)]
2533    pub(crate) fn open_shared_wal_coordination_for_open(
2534        &self,
2535    ) -> Result<Option<Arc<MappedSharedWalCoordination>>> {
2536        self.open_shared_wal_coordination_inner()
2537    }
2538
2539    #[cfg(host_shared_wal)]
2540    fn open_shared_wal_coordination_inner(
2541        &self,
2542    ) -> Result<Option<Arc<MappedSharedWalCoordination>>> {
2543        if !self.opts.enable_multiprocess_wal {
2544            return Ok(None);
2545        }
2546        if !self.io.supports_shared_wal_coordination() {
2547            return Err(LimboError::InvalidArgument(format!(
2548                "experimental multiprocess WAL is not supported by the active IO backend for '{}'",
2549                self.path
2550            )));
2551        }
2552        if is_memory_like(&self.path) || is_memory_like(&self.wal_path) {
2553            return Err(LimboError::InvalidArgument(format!(
2554                "experimental multiprocess WAL is not supported for in-memory database path '{}'",
2555                self.path
2556            )));
2557        }
2558        if !Self::path_allows_shared_wal_coordination(Path::new(&self.path))? {
2559            return Err(LimboError::InvalidArgument(format!(
2560                "experimental multiprocess WAL is not supported on the filesystem backing '{}'",
2561                self.path
2562            )));
2563        }
2564        if let Some(authority) = self.shared_wal_coordination.get() {
2565            return Ok(Some(authority.clone()));
2566        }
2567
2568        let path = storage::wal::coordination_path_for_wal_path(&self.wal_path);
2569        let authority = if self.open_flags.contains(OpenFlags::ReadOnly) {
2570            let Some(authority) = MappedSharedWalCoordination::open_existing(
2571                &self.io,
2572                std::path::Path::new(&path),
2573                64,
2574            )?
2575            else {
2576                // Read-only opens cannot create `.tshm`. If no shared
2577                // coordination file exists, degrade to the legacy read-only WAL
2578                // path rather than failing the open. This keeps binding-level
2579                // option plumbing advisory for readers while writable opens
2580                // still enforce the stricter multiprocess contract.
2581                return Ok(None);
2582            };
2583            Arc::new(authority)
2584        } else {
2585            Arc::new(MappedSharedWalCoordination::create_or_open(
2586                &self.io,
2587                std::path::Path::new(&path),
2588                64,
2589            )?)
2590        };
2591        let _ = self.shared_wal_coordination.set(authority.clone());
2592        Ok(Some(
2593            self.shared_wal_coordination
2594                .get()
2595                .cloned()
2596                .unwrap_or(authority),
2597        ))
2598    }
2599
2600    pub fn shared_wal_open_telemetry(&self) -> Result<SharedWalOpenTelemetry> {
2601        let shared_wal = self.shared_wal.read();
2602        let loaded_from_disk_scan = shared_wal
2603            .metadata
2604            .loaded_from_disk_scan
2605            .load(Ordering::Acquire);
2606        let reopened_max_frame = shared_wal.metadata.max_frame.load(Ordering::Acquire);
2607        let reopened_nbackfills = shared_wal.metadata.nbackfills.load(Ordering::Acquire);
2608        let reopened_checkpoint_seq = shared_wal.metadata.wal_header.lock().checkpoint_seq;
2609        drop(shared_wal);
2610
2611        #[cfg(host_shared_wal)]
2612        let (coordination_open_mode, sanitized_backfill_proof_on_open) =
2613            if let Some(authority) = self.shared_wal_coordination()? {
2614                let mode = match authority.open_mode() {
2615                storage::shared_wal_coordination::SharedWalCoordinationOpenMode::Exclusive => {
2616                    SharedWalCoordinationOpenTelemetryMode::Exclusive
2617                }
2618                storage::shared_wal_coordination::SharedWalCoordinationOpenMode::MultiProcess => {
2619                    SharedWalCoordinationOpenTelemetryMode::MultiProcess
2620                }
2621            };
2622                (Some(mode), authority.sanitized_backfill_proof_on_open())
2623            } else {
2624                (None, false)
2625            };
2626        #[cfg(not(host_shared_wal))]
2627        let (coordination_open_mode, sanitized_backfill_proof_on_open) = (None, false);
2628
2629        Ok(SharedWalOpenTelemetry {
2630            loaded_from_disk_scan,
2631            reopened_max_frame,
2632            reopened_nbackfills,
2633            reopened_checkpoint_seq,
2634            coordination_open_mode,
2635            sanitized_backfill_proof_on_open,
2636        })
2637    }
2638
2639    #[cfg(clt_turso_feature = "simulator")]
2640    pub fn shared_wal_snapshot_for_testing(&self) -> Result<Option<SharedWalTestingSnapshot>> {
2641        #[cfg(host_shared_wal)]
2642        if let Some(authority) = self.shared_wal_coordination()? {
2643            let snapshot = authority.snapshot();
2644            return Ok(Some(SharedWalTestingSnapshot {
2645                max_frame: snapshot.max_frame,
2646                nbackfills: snapshot.nbackfills,
2647                checkpoint_seq: snapshot.checkpoint_seq,
2648                frame_index_overflowed: authority.frame_index_overflowed(),
2649            }));
2650        }
2651
2652        Ok(None)
2653    }
2654
2655    #[cfg(clt_turso_feature = "simulator")]
2656    pub fn shared_wal_find_frame_for_testing(&self, page_id: u64) -> Result<Option<u64>> {
2657        #[cfg(host_shared_wal)]
2658        if let Some(authority) = self.shared_wal_coordination()? {
2659            let snapshot = authority.snapshot();
2660            return Ok(authority.find_frame(page_id, 0, snapshot.max_frame, None));
2661        }
2662
2663        Ok(None)
2664    }
2665
2666    #[cfg(clt_turso_feature = "simulator")]
2667    pub fn local_wal_find_frame_for_testing(&self, page_id: u64) -> Result<Option<u64>> {
2668        let shared = self.shared_wal.read();
2669        let max_frame = shared.metadata.max_frame.load(Ordering::Acquire);
2670        let frame_cache = shared.runtime.frame_cache.lock();
2671        Ok(frame_cache.get(&page_id).and_then(|frames| {
2672            frames
2673                .iter()
2674                .rfind(|&&frame_id| frame_id <= max_frame)
2675                .copied()
2676        }))
2677    }
2678
2679    #[cfg(clt_turso_feature = "simulator")]
2680    pub fn local_wal_max_frame_for_testing(&self) -> Result<u64> {
2681        Ok(self
2682            .shared_wal
2683            .read()
2684            .metadata
2685            .max_frame
2686            .load(Ordering::Acquire))
2687    }
2688
2689    #[cfg(clt_turso_feature = "simulator")]
2690    pub fn clear_backfill_proof_for_testing(&self) -> Result<()> {
2691        #[cfg(host_shared_wal)]
2692        {
2693            let authority = self.shared_wal_coordination()?.ok_or_else(|| {
2694                LimboError::InternalError("shared WAL authority is unavailable".into())
2695            })?;
2696            authority.clear_backfill_proof();
2697            Ok(())
2698        }
2699
2700        #[cfg(not(host_shared_wal))]
2701        {
2702            Err(LimboError::InternalError(
2703                "shared WAL authority is unavailable on this platform".into(),
2704            ))
2705        }
2706    }
2707
2708    fn build_wal(
2709        &self,
2710        last_checksum_and_max_frame: ((u32, u32), u64),
2711        buffer_pool: Arc<BufferPool>,
2712    ) -> Result<Arc<dyn Wal>> {
2713        #[cfg(host_shared_wal)]
2714        if let Some(authority) = self.shared_wal_coordination()? {
2715            return Ok(Arc::new(WalFile::new_with_shared_coordination(
2716                self.io.clone(),
2717                self.shared_wal.clone(),
2718                authority,
2719                last_checksum_and_max_frame,
2720                buffer_pool,
2721            )));
2722        }
2723
2724        Ok(Arc::new(WalFile::new(
2725            self.io.clone(),
2726            self.shared_wal.clone(),
2727            last_checksum_and_max_frame,
2728            buffer_pool,
2729        )))
2730    }
2731
2732    fn init_pager(
2733        &self,
2734        requested_page_size: Option<usize>,
2735        hdr_st: &mut DbHeaderReadState,
2736    ) -> Result<IOResult<Pager>> {
2737        let cipher = self.encryption_cipher_mode.get();
2738
2739        // For an existing (initialized) database, read the 512-byte header
2740        // once (non-blocking) and recover both the reserved-space byte and the
2741        // on-disk page size from it.
2742        let (header_reserved_bytes, header_page_size) = if self.initialized() {
2743            let buf = return_if_io!(self.read_db_header_buf(hdr_st));
2744            let reserved = u8::from_be_bytes(buf.as_slice()[20..21].try_into().unwrap());
2745            let ps_raw = u16::from_be_bytes(buf.as_slice()[16..18].try_into().unwrap());
2746            let page_size = PageSize::new_from_header_u16(ps_raw)?;
2747            (Some(reserved), Some(page_size))
2748        } else {
2749            (None, None)
2750        };
2751
2752        let reserved_bytes = header_reserved_bytes.or_else(|| {
2753            if !matches!(cipher, CipherMode::None) {
2754                // For encryption, use the cipher's metadata size
2755                Some(cipher.metadata_size() as u8)
2756            } else {
2757                None
2758            }
2759        });
2760        let disable_checksums = if let Some(reserved_bytes) = reserved_bytes {
2761            // if the required reserved bytes for checksums is not present, disable checksums
2762            reserved_bytes != CHECKSUM_REQUIRED_RESERVED_BYTES
2763        } else {
2764            false
2765        };
2766        // Check if WAL is enabled
2767        let shared_wal = self.shared_wal.read();
2768
2769        let page_size =
2770            self.determine_actual_page_size(&shared_wal, requested_page_size, header_page_size)?;
2771
2772        let buffer_pool = self.buffer_pool.clone();
2773        if self.initialized() {
2774            buffer_pool.finalize_with_page_size(page_size.get() as usize)?;
2775        }
2776
2777        let wal_enabled = shared_wal.metadata.enabled.load(Ordering::SeqCst);
2778        let last_checksum_and_max_frame = shared_wal.last_checksum_and_max_frame();
2779        drop(shared_wal);
2780        let pager_wal: Option<Arc<dyn Wal>> = if wal_enabled {
2781            Some(self.build_wal(last_checksum_and_max_frame, buffer_pool.clone())?)
2782        } else {
2783            None
2784        };
2785
2786        let pager = Pager::new(
2787            self.db_file.clone(),
2788            pager_wal,
2789            self.io.clone(),
2790            PageCache::default(),
2791            buffer_pool,
2792            self.init_lock.clone(),
2793            self.init_page_1.clone(),
2794        )?;
2795        pager.set_page_size(page_size);
2796        if let Some(reserved_bytes) = reserved_bytes {
2797            pager.set_reserved_space_bytes(reserved_bytes);
2798        }
2799        if disable_checksums {
2800            pager.reset_checksum_context();
2801        }
2802
2803        Ok(IOResult::Done(pager))
2804    }
2805
2806    #[cfg(clt_turso_feature = "fs")]
2807    pub fn io_for_path(path: &str) -> Result<Arc<dyn IO>> {
2808        let io: Arc<dyn IO> = if is_memory_like(path.trim()) {
2809            Arc::new(MemoryIO::new())
2810        } else {
2811            Arc::new(PlatformIO::new()?)
2812        };
2813        Ok(io)
2814    }
2815
2816    #[cfg(clt_turso_feature = "fs")]
2817    pub fn io_for_vfs<S: AsRef<str> + std::fmt::Display>(vfs: S) -> Result<Arc<dyn IO>> {
2818        if let Some(io) = crate::io::get_registered_io(vfs.as_ref()) {
2819            return Ok(io);
2820        }
2821        let vfsmods = ext::add_builtin_vfs_extensions(None)?;
2822        let io: Arc<dyn IO> = match vfsmods
2823            .iter()
2824            .find(|v| v.0 == vfs.as_ref())
2825            .map(|v| v.1.clone())
2826        {
2827            Some(vfs) => vfs,
2828            None => match vfs.as_ref() {
2829                "memory" => Arc::new(MemoryIO::new()),
2830                #[cfg(clt_turso_feature = "io_memory_yield")]
2831                "memory_yield" => Arc::new(MemoryYieldIO::new()),
2832                "syscall" => Arc::new(SyscallIO::new()?),
2833                #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring", not(miri)))]
2834                "io_uring" => Arc::new(UringIO::new()?),
2835                #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp", not(miri)))]
2836                "experimental_win_iocp" => Arc::new(WindowsIOCP::new()?),
2837
2838                other => {
2839                    return Err(LimboError::InvalidArgument(format!("no such VFS: {other}")));
2840                }
2841            },
2842        };
2843        Ok(io)
2844    }
2845
2846    /// Open a new database file with optionally specifying a VFS without an existing database
2847    /// connection and symbol table to register extensions.
2848    #[cfg(clt_turso_feature = "fs")]
2849    pub fn open_new<S>(
2850        path: &str,
2851        vfs: Option<S>,
2852        flags: OpenFlags,
2853        opts: DatabaseOpts,
2854        encryption_opts: Option<EncryptionOpts>,
2855    ) -> Result<(Arc<dyn IO>, Arc<Database>)>
2856    where
2857        S: AsRef<str> + std::fmt::Display,
2858    {
2859        let io = vfs
2860            .map(|vfs| Self::io_for_vfs(vfs))
2861            .or_else(|| Some(Self::io_for_path(path)))
2862            .transpose()?
2863            .unwrap();
2864        let db = Self::open_file_with_flags(io.clone(), path, flags, opts, encryption_opts)?;
2865        Ok((io, db))
2866    }
2867
2868    #[inline]
2869    pub(crate) fn initialized(&self) -> bool {
2870        self.init_page_1.load().is_none()
2871    }
2872
2873    pub(crate) fn can_load_extensions(&self) -> bool {
2874        self.opts.enable_load_extension
2875    }
2876
2877    #[inline]
2878    pub(crate) fn with_schema_mut<T>(&self, f: impl FnOnce(&mut Schema) -> Result<T>) -> Result<T> {
2879        let mut schema_ref = self.schema.lock();
2880        let schema = Schema::try_make_mut(&mut schema_ref)?;
2881        f(schema)
2882    }
2883
2884    pub(crate) fn replace_schema(&self, schema: Arc<Schema>) {
2885        *self.schema.lock() = schema;
2886    }
2887
2888    /// Register an `InternalVirtualTable` into this database's catalog. The
2889    /// table is visible to connections opened after this call and is queryable
2890    /// like any other table.
2891    ///
2892    /// Intended for callers that want to surface state as a queryable table
2893    /// without going through `CREATE VIRTUAL TABLE` — for example, extensions
2894    /// contributing metadata tables or alternative-dialect catalogs.
2895    ///
2896    /// Call before opening connections. Connections that already exist will
2897    /// not pick up the new table unless they re-read the shared schema (e.g.
2898    /// via the usual schema-change path).
2899    pub fn register_internal_vtab<T>(&self, table: T) -> Result<String>
2900    where
2901        T: InternalVirtualTable + 'static,
2902    {
2903        self.with_schema_mut(|schema| schema.register_internal_vtab(table))
2904    }
2905    pub(crate) fn clone_schema(&self) -> Arc<Schema> {
2906        let schema = self.schema.lock();
2907        schema.clone()
2908    }
2909
2910    pub(crate) fn update_schema_if_newer(&self, another: Arc<Schema>) {
2911        let mut schema = self.schema.lock();
2912        if schema.schema_version < another.schema_version {
2913            tracing::debug!(
2914                "DB schema is outdated: {} < {}",
2915                schema.schema_version,
2916                another.schema_version
2917            );
2918            *schema = another;
2919        } else {
2920            tracing::debug!(
2921                "DB schema is up to date: {} >= {}",
2922                schema.schema_version,
2923                another.schema_version
2924            );
2925        }
2926    }
2927
2928    pub fn get_mv_store(&self) -> impl Deref<Target = Option<Arc<MvStore>>> {
2929        self.mv_store.load()
2930    }
2931
2932    pub fn experimental_views_enabled(&self) -> bool {
2933        self.opts.enable_views
2934    }
2935
2936    pub fn experimental_index_method_enabled(&self) -> bool {
2937        self.opts.enable_index_method
2938    }
2939
2940    pub fn experimental_custom_types_enabled(&self) -> bool {
2941        self.opts.enable_custom_types
2942    }
2943
2944    pub fn experimental_encryption_enabled(&self) -> bool {
2945        self.opts.enable_encryption
2946    }
2947
2948    pub fn experimental_autovacuum_enabled(&self) -> bool {
2949        self.opts.enable_autovacuum
2950    }
2951
2952    pub fn experimental_vacuum_enabled(&self) -> bool {
2953        self.opts.enable_vacuum
2954    }
2955
2956    pub fn experimental_mvcc_passive_checkpoint_enabled(&self) -> bool {
2957        self.opts.enable_experimental_mvcc_passive_checkpoint
2958    }
2959
2960    pub fn experimental_attach_enabled(&self) -> bool {
2961        self.opts.enable_attach
2962    }
2963
2964    pub fn experimental_generated_columns_enabled(&self) -> bool {
2965        self.opts.enable_generated_columns
2966    }
2967
2968    pub fn experimental_multiprocess_wal_enabled(&self) -> bool {
2969        self.opts.enable_multiprocess_wal
2970    }
2971
2972    pub fn experimental_without_rowid_enabled(&self) -> bool {
2973        self.opts.enable_without_rowid
2974    }
2975
2976    /// check if database is currently in MVCC mode
2977    pub fn mvcc_enabled(&self) -> bool {
2978        self.mv_store.load().is_some()
2979    }
2980
2981    #[cfg(clt_turso_feature = "test_helper")]
2982    pub fn set_pending_byte(val: u32) {
2983        Pager::set_pending_byte(val);
2984    }
2985
2986    #[cfg(clt_turso_feature = "test_helper")]
2987    pub fn get_pending_byte() -> u32 {
2988        Pager::get_pending_byte()
2989    }
2990}
2991
2992#[derive(Debug, Clone, Eq, PartialEq)]
2993pub enum CaptureDataChangesMode {
2994    Id,
2995    Before,
2996    After,
2997    Full,
2998}
2999
3000/// CDC schema version with integer ordering for feature checks.
3001/// Higher versions are supersets of lower versions.
3002#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
3003#[repr(u8)]
3004pub enum CdcVersion {
3005    /// 8 columns: change_id, change_time, change_type, table_name, id, before, after, updates
3006    V1 = 1,
3007    /// 9 columns (adds change_txn_id + COMMIT records with change_type=2)
3008    V2 = 2,
3009}
3010
3011pub const CDC_VERSION_CURRENT: CdcVersion = CdcVersion::V2;
3012
3013impl CdcVersion {
3014    /// Whether this version emits COMMIT records (change_type=2)
3015    pub fn has_commit_record(self) -> bool {
3016        self >= CdcVersion::V2
3017    }
3018}
3019
3020impl std::fmt::Display for CdcVersion {
3021    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3022        match self {
3023            CdcVersion::V1 => write!(f, "v1"),
3024            CdcVersion::V2 => write!(f, "v2"),
3025        }
3026    }
3027}
3028
3029impl std::str::FromStr for CdcVersion {
3030    type Err = LimboError;
3031    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
3032        match s {
3033            "v1" => Ok(CdcVersion::V1),
3034            "v2" => Ok(CdcVersion::V2),
3035            _ => Err(LimboError::InternalError(format!(
3036                "unexpected CDC version: {s}"
3037            ))),
3038        }
3039    }
3040}
3041
3042#[derive(Debug, Clone, Eq, PartialEq)]
3043pub struct CaptureDataChangesInfo {
3044    pub mode: CaptureDataChangesMode,
3045    pub table: String,
3046    pub version: Option<CdcVersion>,
3047}
3048
3049impl CaptureDataChangesInfo {
3050    pub fn parse(
3051        value: &str,
3052        version: Option<CdcVersion>,
3053    ) -> Result<Option<CaptureDataChangesInfo>> {
3054        let (mode, table) = value
3055            .split_once(",")
3056            .unwrap_or((value, TURSO_CDC_DEFAULT_TABLE_NAME));
3057        match mode {
3058            "off" => Ok(None),
3059            "id" => Ok(Some(CaptureDataChangesInfo { mode: CaptureDataChangesMode::Id, table: table.to_string(), version })),
3060            "before" => Ok(Some(CaptureDataChangesInfo { mode: CaptureDataChangesMode::Before, table: table.to_string(), version })),
3061            "after" => Ok(Some(CaptureDataChangesInfo { mode: CaptureDataChangesMode::After, table: table.to_string(), version })),
3062            "full" => Ok(Some(CaptureDataChangesInfo { mode: CaptureDataChangesMode::Full, table: table.to_string(), version })),
3063            _ => Err(LimboError::InvalidArgument(
3064                "unexpected pragma value: expected '<mode>' or '<mode>,<cdc-table-name>' parameter where mode is one of off|id|before|after|full".to_string(),
3065            ))
3066        }
3067    }
3068    pub fn has_updates(&self) -> bool {
3069        self.mode == CaptureDataChangesMode::Full
3070    }
3071    pub fn has_after(&self) -> bool {
3072        matches!(
3073            self.mode,
3074            CaptureDataChangesMode::After | CaptureDataChangesMode::Full
3075        )
3076    }
3077    pub fn has_before(&self) -> bool {
3078        matches!(
3079            self.mode,
3080            CaptureDataChangesMode::Before | CaptureDataChangesMode::Full
3081        )
3082    }
3083    pub fn mode_name(&self) -> &str {
3084        match self.mode {
3085            CaptureDataChangesMode::Id => "id",
3086            CaptureDataChangesMode::Before => "before",
3087            CaptureDataChangesMode::After => "after",
3088            CaptureDataChangesMode::Full => "full",
3089        }
3090    }
3091    pub fn cdc_version(&self) -> CdcVersion {
3092        self.version.unwrap_or(CDC_VERSION_CURRENT)
3093    }
3094}
3095
3096/// Convenience methods for `Option<CaptureDataChangesInfo>` to keep call sites simple.
3097pub trait CaptureDataChangesExt {
3098    fn has_updates(&self) -> bool;
3099    fn has_after(&self) -> bool;
3100    fn has_before(&self) -> bool;
3101    fn table(&self) -> Option<&str>;
3102}
3103
3104impl CaptureDataChangesExt for Option<CaptureDataChangesInfo> {
3105    fn has_updates(&self) -> bool {
3106        self.as_ref().is_some_and(|i| i.has_updates())
3107    }
3108    fn has_after(&self) -> bool {
3109        self.as_ref().is_some_and(|i| i.has_after())
3110    }
3111    fn has_before(&self) -> bool {
3112        self.as_ref().is_some_and(|i| i.has_before())
3113    }
3114    fn table(&self) -> Option<&str> {
3115        self.as_ref().map(|i| i.table.as_str())
3116    }
3117}
3118
3119// Optimized for fast get() operations and supports unlimited attached databases.
3120pub(crate) struct DatabaseCatalog {
3121    name_to_index: HashMap<String, usize>,
3122    allocated: Vec<u64>,
3123    index_to_data: HashMap<usize, (Arc<Database>, Arc<Pager>)>,
3124}
3125
3126#[allow(unused)]
3127impl DatabaseCatalog {
3128    pub(crate) fn new() -> Self {
3129        Self {
3130            name_to_index: HashMap::default(),
3131            index_to_data: HashMap::default(),
3132            allocated: vec![3], // 0 | 1, as those are reserved for main and temp
3133        }
3134    }
3135
3136    fn get_database_by_index(&self, index: usize) -> Option<Arc<Database>> {
3137        self.index_to_data
3138            .get(&index)
3139            .map(|(db, _pager)| db.clone())
3140    }
3141
3142    fn get_name_by_index(&self, index: usize) -> Option<String> {
3143        self.name_to_index
3144            .iter()
3145            .find(|(_, &idx)| idx == index)
3146            .map(|(name, _)| name.clone())
3147    }
3148
3149    fn get_database_by_name(&self, s: &str) -> Option<(usize, Arc<Database>)> {
3150        match self.name_to_index.get(s) {
3151            None => None,
3152            Some(idx) => self
3153                .index_to_data
3154                .get(idx)
3155                .map(|(db, _pager)| (*idx, db.clone())),
3156        }
3157    }
3158
3159    fn get_pager_by_index(&self, idx: &usize) -> Arc<Pager> {
3160        let (_db, pager) = self
3161            .index_to_data
3162            .get(idx)
3163            .expect("If we are looking up a database by index, it must exist.");
3164        pager.clone()
3165    }
3166
3167    fn add(&mut self, s: &str) -> usize {
3168        turso_assert!(
3169            !self.name_to_index.contains_key(s),
3170            "lib: database name already exists in catalog",
3171            { "name": s }
3172        );
3173
3174        let index = self.allocate_index();
3175        self.name_to_index.insert(s.to_string(), index);
3176        index
3177    }
3178
3179    fn insert(&mut self, s: &str, data: (Arc<Database>, Arc<Pager>)) -> usize {
3180        let idx = self.add(s);
3181        self.index_to_data.insert(idx, data);
3182        idx
3183    }
3184
3185    fn remove(&mut self, s: &str) -> Option<usize> {
3186        if let Some(index) = self.name_to_index.remove(s) {
3187            // Should be impossible to remove main or temp.
3188            turso_assert_greater_than_or_equal!(index, 2);
3189            self.deallocate_index(index);
3190            self.index_to_data.remove(&index);
3191            Some(index)
3192        } else {
3193            None
3194        }
3195    }
3196
3197    #[inline(always)]
3198    fn deallocate_index(&mut self, index: usize) {
3199        let word_idx = index / 64;
3200        let bit_idx = index % 64;
3201
3202        if word_idx < self.allocated.len() {
3203            self.allocated[word_idx] &= !(1u64 << bit_idx);
3204        }
3205    }
3206
3207    fn allocate_index(&mut self) -> usize {
3208        for word_idx in 0..self.allocated.len() {
3209            let word = self.allocated[word_idx];
3210
3211            if word != u64::MAX {
3212                let free_bit = Self::find_first_zero_bit(word);
3213                let index = word_idx * 64 + free_bit;
3214
3215                self.allocated[word_idx] |= 1u64 << free_bit;
3216
3217                return index;
3218            }
3219        }
3220
3221        // Need to expand bitmap
3222        let word_idx = self.allocated.len();
3223        self.allocated.push(1u64); // Mark first bit as allocated
3224        word_idx * 64
3225    }
3226
3227    #[inline(always)]
3228    fn find_first_zero_bit(word: u64) -> usize {
3229        // Invert to find first zero as first one
3230        let inverted = !word;
3231
3232        // Use trailing zeros count (compiles to single instruction on most CPUs)
3233        inverted.trailing_zeros() as usize
3234    }
3235}
3236
3237pub struct QueryRunner<'a> {
3238    parser: Parser<'a>,
3239    conn: &'a Arc<Connection>,
3240    statements: &'a [u8],
3241    last_offset: usize,
3242}
3243
3244impl<'a> QueryRunner<'a> {
3245    pub(crate) fn new(conn: &'a Arc<Connection>, statements: &'a [u8]) -> Self {
3246        Self {
3247            parser: Parser::new(statements),
3248            conn,
3249            statements,
3250            last_offset: 0,
3251        }
3252    }
3253}
3254
3255impl Iterator for QueryRunner<'_> {
3256    type Item = Result<Option<Statement>>;
3257
3258    fn next(&mut self) -> Option<Self::Item> {
3259        match self.parser.next_cmd() {
3260            Ok(Some(cmd)) => {
3261                let byte_offset_end = self.parser.offset();
3262                let input = str::from_utf8(&self.statements[self.last_offset..byte_offset_end])
3263                    .unwrap()
3264                    .trim();
3265                self.last_offset = byte_offset_end;
3266                Some(self.conn.run_cmd(cmd, input))
3267            }
3268            Ok(None) => None,
3269            Err(err) => Some(Result::Err(LimboError::from(err))),
3270        }
3271    }
3272}
3273
3274#[cfg(clt_turso_tests)]
3275mod database_tests {
3276    use super::{is_memory_like, Database};
3277
3278    #[test]
3279    fn memory_path_classifies_named_memory_databases() {
3280        assert!(is_memory_like(":memory:"));
3281        assert!(is_memory_like(":memory:sync-draft"));
3282        assert!(is_memory_like("file::memory:?cache=shared"));
3283        assert!(is_memory_like(""));
3284        assert!(!is_memory_like("memory.db"));
3285        assert!(!is_memory_like("file:memory.db"));
3286    }
3287
3288    #[cfg(clt_turso_feature = "fs")]
3289    #[test]
3290    fn io_for_path_uses_memory_io_for_named_memory_database() {
3291        let path = format!(":memory:named-io-selection-{}", std::process::id());
3292        assert!(std::fs::metadata(&path).is_err());
3293
3294        let io = Database::io_for_path(&path).unwrap();
3295
3296        assert!(io.file_id(&path).is_ok());
3297        assert!(std::fs::metadata(&path).is_err());
3298    }
3299}
3300
3301// The engine and SDK are targets/modules of CLT's single Cargo package.
3302pub extern crate self as turso_core;
3303#[path = "../turso_sdk_kit/src/lib.rs"]
3304pub mod turso_sdk_kit;
3305#[path = "../turso/src/lib.rs"]
3306pub mod turso;