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#![allow(warnings, clippy::all)]
16
17pub 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
208pub const MAIN_DB_ID: usize = 0;
210
211mod turso_types_vtab;
212
213pub const TEMP_DB_ID: usize = 1;
215
216pub const FIRST_ATTACHED_DB_ID: usize = 2;
220
221pub const INVALID_DB_ID: usize = usize::MAX;
230
231pub const fn is_main_or_temp_db(database_id: usize) -> bool {
233 database_id == MAIN_DB_ID || database_id == TEMP_DB_ID
234}
235
236pub const fn is_attached_db(database_id: usize) -> bool {
239 database_id >= FIRST_ATTACHED_DB_ID
240}
241
242#[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#[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
395fn is_memory_like(path: &str) -> bool {
400 path.starts_with(":memory:") || path.starts_with("file::memory:") || path.is_empty()
401}
402
403fn 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; };
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, expected,
418 actual: bytes_read as usize,
419 });
420 }
421 None
422 })
423}
424
425#[derive(Default, Debug)]
427pub enum OpenDbAsyncPhase {
428 #[default]
429 Init,
430 ValidatingHeader,
433 ReadingHeader,
434 LoadingSchema,
435 BootstrapMvStore,
436 Done,
437}
438
439#[derive(Default)]
446pub(crate) enum DbHeaderReadState {
447 #[default]
448 Start,
449 Reading {
450 buf: Arc<Buffer>,
451 completion: Completion,
452 },
453}
454
455#[derive(Default)]
460pub(crate) enum InitState {
461 #[default]
462 Start,
463 InitPager(DbHeaderReadState),
465 ReadPage1 { pager: Box<Pager> },
467}
468
469enum HeaderValidationState {
473 Start {
474 init: InitState,
475 },
476 Validate {
482 pager: Box<Pager>,
483 is_readonly: bool,
484 log_exists: bool,
485 },
486 WriteHeader {
489 pager: Box<Pager>,
490 page: PageRef,
491 open_mv_store: bool,
492 completion: Option<Completion>,
493 },
494 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
512pub 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_guard: Option<sync::ArcMutexGuard<Arc<Schema>>>,
522 registry_key: Option<DatabaseKey>,
524 building_db: Option<Database>,
527 header_validation_state: HeaderValidationState,
529 mvcc_bootstrap_conn: Option<Arc<Connection>>,
532 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(®istry_key);
567 }
568 }
569}
570
571enum RegistryEntry {
573 Opening,
576 Ready(Weak<Database>),
578}
579
580#[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
614pub 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_page_cache: Arc<RwLock<PageCache>>,
631
632 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 builtin_syms: parking_lot::RwLock<SymbolTable>,
645 opts: DatabaseOpts,
646 n_connections: AtomicUsize,
647
648 init_page_1: Arc<ArcSwapOption<Page>>,
650
651 encryption_cipher_mode: AtomicCipherMode,
653}
654
655crate::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 let db_state_value = match &*self.init_page_1.load() {
668 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 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 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 #[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 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 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 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), };
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 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 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 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 Self::reject_live_legacy_wal_for_multiprocess_open(&io, path, flags, opts)?;
1036 let file = io.open_file(path, effective_flags, true)?;
1037
1038 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 #[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 #[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 if let Some(registry_key) = state.registry_key.take() {
1186 let mut registry = DATABASE_MANAGER.lock();
1187 registry.remove(®istry_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 if matches!(state.phase, OpenDbAsyncPhase::Init) && !is_memory_like(path) {
1210 let mut registry = DATABASE_MANAGER.lock();
1212
1213 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 registry.insert(key.clone(), RegistryEntry::Opening);
1234 }
1235 Some(RegistryEntry::Opening) => {
1236 return Ok(IOResult::IO(types::IOCompletions::Single(
1239 io::Completion::new_yield(),
1240 )));
1241 }
1242 None => {
1243 registry.insert(key.clone(), RegistryEntry::Opening);
1245 }
1246 }
1247 state.registry_key = Some(key);
1248 }
1249 }
1252
1253 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 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 #[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 #[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 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 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 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 let db = Arc::new(db);
1459
1460 let conn =
1462 db._connect(false, Some(pager.clone()), state.encryption_key.clone())?;
1463
1464 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 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 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 state.schema_guard = None;
1533 }
1534 Err(LimboError::ExtensionError(e)) => {
1535 state.schema_guard = None;
1538 tracing::warn!("open warning, failed to load extension: {e}");
1539 }
1540 Err(e) => return Err(e),
1541 }
1542
1543 if opts.enable_custom_types {
1550 let conn = state
1551 .conn
1552 .as_ref()
1553 .expect("conn must be initialized in Init phase");
1554 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 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 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 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 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 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 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 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 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 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 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 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 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 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 !(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 let open_mv_store = matches!(read_version, Version::Mvcc);
1876
1877 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 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 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 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 drop(header);
1925
1926 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 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 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 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 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 String::new()
2069 } else {
2070 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 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 #[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 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 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 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 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 !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 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 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 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 Some(cipher.metadata_size() as u8)
2770 } else {
2771 None
2772 }
2773 });
2774 let disable_checksums = if let Some(reserved_bytes) = reserved_bytes {
2775 reserved_bytes != CHECKSUM_REQUIRED_RESERVED_BYTES
2777 } else {
2778 false
2779 };
2780 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 #[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 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 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#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
3021#[repr(u8)]
3022pub enum CdcVersion {
3023 V1 = 1,
3025 V2 = 2,
3027}
3028
3029pub const CDC_VERSION_CURRENT: CdcVersion = CdcVersion::V2;
3030
3031impl CdcVersion {
3032 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
3114pub 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
3137pub(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], }
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 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 let word_idx = self.allocated.len();
3241 self.allocated.push(1u64); word_idx * 64
3243 }
3244
3245 #[inline(always)]
3246 fn find_first_zero_bit(word: u64) -> usize {
3247 let inverted = !word;
3249
3250 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
3319pub 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;