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