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