Skip to main content

turso_orm_driver/
options.rs

1//! The options a database is opened with, modeled by [`ConnectOptions`].
2//!
3//! The options cover three layers that the rest of the crate keeps apart:
4//! how the engine opens the file (source, read-only, encryption, the
5//! experimental feature flags of `turso::Builder`), how the pool behaves
6//! (size and acquire timeout) and what every new connection is configured
7//! with (busy timeout, `foreign_keys`, MVCC and arbitrary pragmas). This
8//! module owns the data and the translation into a `turso::Builder`; it
9//! does not open anything, which is [`Database::connect`]'s job.
10//!
11//! Defaults follow what an application usually wants rather than SQLite's
12//! own: foreign keys are enforced, a lock is waited on for five seconds and
13//! the pool holds eight connections.
14//!
15//! - [`ConnectOptions`]: the builder;
16//! - [`Source`], [`Encryption`], [`Experimental`]: the pieces of it;
17//! - `SyncOptions`: the embedded-replica settings, behind the `sync`
18//!   feature;
19//! - `RemoteOptions`: the Turso Cloud HTTP settings, behind the
20//!   `serverless` feature.
21//!
22//! [`Database::connect`]: crate::Database::connect
23
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26
27/// Where the database lives.
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Source {
31    /// A private in-memory database shared by every connection of the pool.
32    Memory,
33    /// A local database file, created if missing.
34    File(PathBuf),
35    /// A local file kept in sync with a Turso Cloud database — an embedded
36    /// replica.
37    #[cfg(feature = "sync")]
38    #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
39    Sync(SyncOptions),
40    /// A Turso Cloud database reached over HTTP, with no local file.
41    #[cfg(feature = "serverless")]
42    #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
43    Remote(RemoteOptions),
44}
45
46/// The settings of a remote Turso Cloud database.
47#[cfg(feature = "serverless")]
48#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
49#[derive(Clone, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct RemoteOptions {
52    /// The database URL (`libsql://`, `turso://` or `https://`).
53    pub url: String,
54    /// The bearer token for the database.
55    pub auth_token: Option<String>,
56    /// The base64 key of a database encrypted with a customer-managed key.
57    pub remote_encryption_key: Option<String>,
58}
59
60/// The settings of an embedded replica synchronised with Turso Cloud.
61#[cfg(feature = "sync")]
62#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct SyncOptions {
65    /// The local replica file.
66    pub path: PathBuf,
67    /// The remote database URL (`libsql://`, `turso://` or `https://`).
68    pub remote_url: String,
69    /// The bearer token for the remote database.
70    pub auth_token: Option<String>,
71    /// Whether to download the remote database on first open when the local
72    /// file is empty.
73    pub bootstrap_if_empty: bool,
74}
75
76/// The encryption-at-rest settings — experimental in Turso.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct Encryption {
79    /// The cipher name, for example `aes256gcm`.
80    pub cipher: String,
81    /// The hex-encoded key.
82    pub hexkey: String,
83}
84
85/// Turso engine features that are opt-in because they are still
86/// experimental.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88#[non_exhaustive]
89#[allow(
90    clippy::struct_excessive_bools,
91    reason = "the flags mirror the independent flags of turso::Builder"
92)]
93pub struct Experimental {
94    /// `ATTACH` and `DETACH`.
95    pub attach: bool,
96    /// `CREATE TYPE`, `CREATE DOMAIN` and array column types.
97    pub custom_types: bool,
98    /// Virtual generated columns.
99    pub generated_columns: bool,
100    /// `CREATE INDEX ... USING fts` and vector indexes.
101    pub index_method: bool,
102    /// `CREATE MATERIALIZED VIEW`.
103    pub materialized_views: bool,
104    /// In-place `VACUUM`.
105    pub vacuum: bool,
106    /// Several OS processes opening the same file.
107    pub multiprocess_wal: bool,
108    /// `WITHOUT ROWID` tables.
109    pub without_rowid: bool,
110}
111
112/// The options for opening a Turso database.
113///
114/// ```
115/// use std::time::Duration;
116/// use turso_orm_driver::ConnectOptions;
117///
118/// let opts = ConnectOptions::new("app.db")
119///     .max_connections(4)
120///     .busy_timeout(Duration::from_secs(5))
121///     .foreign_keys(true);
122/// assert_eq!(opts.max_connections_value(), 4);
123/// ```
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct ConnectOptions {
126    /// Where the database lives.
127    pub(crate) source: Source,
128    /// Whether to open read-only.
129    pub(crate) read_only: bool,
130    /// The encryption-at-rest settings, if any.
131    pub(crate) encryption: Option<Encryption>,
132    /// The experimental engine features to enable.
133    pub(crate) experimental: Experimental,
134    /// The pool size, at least one.
135    pub(crate) max_connections: usize,
136    /// How long to wait for a free pooled connection.
137    pub(crate) acquire_timeout: Duration,
138    /// How long a connection waits on a lock; `None` fails immediately.
139    pub(crate) busy_timeout: Option<Duration>,
140    /// Whether to enforce foreign keys on every connection.
141    pub(crate) foreign_keys: bool,
142    /// Whether to switch the journal mode to MVCC on every connection.
143    pub(crate) mvcc: bool,
144    /// Extra `PRAGMA name = value` pairs run on every new connection.
145    pub(crate) pragmas: Vec<(String, String)>,
146}
147
148impl ConnectOptions {
149    /// The defaults for any source.
150    fn with_source(source: Source) -> Self {
151        Self {
152            source,
153            read_only: false,
154            encryption: None,
155            experimental: Experimental::default(),
156            max_connections: 8,
157            acquire_timeout: Duration::from_secs(30),
158            busy_timeout: Some(Duration::from_secs(5)),
159            foreign_keys: true,
160            mvcc: false,
161            pragmas: Vec::new(),
162        }
163    }
164
165    /// Opens, or creates, the database file at `path`; `":memory:"` opens
166    /// an in-memory database.
167    pub fn new(path: impl AsRef<Path>) -> Self {
168        let path = path.as_ref();
169        if path.as_os_str() == ":memory:" {
170            Self::in_memory()
171        } else {
172            Self::with_source(Source::File(path.to_path_buf()))
173        }
174    }
175
176    /// Opens a private in-memory database.
177    pub fn in_memory() -> Self {
178        Self::with_source(Source::Memory)
179    }
180
181    /// Opens a local replica of a Turso Cloud database.
182    #[cfg(feature = "sync")]
183    #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
184    pub fn sync(path: impl AsRef<Path>, remote_url: impl Into<String>) -> Self {
185        Self::with_source(Source::Sync(SyncOptions {
186            path: path.as_ref().to_path_buf(),
187            remote_url: remote_url.into(),
188            auth_token: None,
189            bootstrap_if_empty: true,
190        }))
191    }
192
193    /// Opens a Turso Cloud database over HTTP, without a local file.
194    ///
195    /// Every statement is an HTTP request, so the busy timeout and the
196    /// MVCC setting do not apply; the pool, the pragmas and the
197    /// transactions do, since each connection is a server-side session.
198    #[cfg(feature = "serverless")]
199    #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
200    pub fn remote(url: impl Into<String>) -> Self {
201        Self::with_source(Source::Remote(RemoteOptions {
202            url: url.into(),
203            auth_token: None,
204            remote_encryption_key: None,
205        }))
206    }
207
208    /// Sets the bearer token of a remote database or of an embedded
209    /// replica's remote; ignored for local sources.
210    #[cfg(any(feature = "sync", feature = "serverless"))]
211    #[cfg_attr(docsrs, doc(cfg(any(feature = "sync", feature = "serverless"))))]
212    #[must_use]
213    pub fn auth_token(mut self, token: impl Into<String>) -> Self {
214        match &mut self.source {
215            #[cfg(feature = "sync")]
216            Source::Sync(opts) => opts.auth_token = Some(token.into()),
217            #[cfg(feature = "serverless")]
218            Source::Remote(opts) => opts.auth_token = Some(token.into()),
219            _ => {}
220        }
221        self
222    }
223
224    /// Sets the base64 key of a remote database encrypted with a
225    /// customer-managed key; ignored for other sources.
226    #[cfg(feature = "serverless")]
227    #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
228    #[must_use]
229    pub fn remote_encryption_key(mut self, key: impl Into<String>) -> Self {
230        if let Source::Remote(opts) = &mut self.source {
231            opts.remote_encryption_key = Some(key.into());
232        }
233        self
234    }
235
236    /// Opens the database in read-only mode.
237    #[must_use]
238    pub fn read_only(mut self, read_only: bool) -> Self {
239        self.read_only = read_only;
240        self
241    }
242
243    /// Enables encryption at rest — experimental in Turso.
244    #[must_use]
245    pub fn encryption(mut self, cipher: impl Into<String>, hexkey: impl Into<String>) -> Self {
246        self.encryption = Some(Encryption {
247            cipher: cipher.into(),
248            hexkey: hexkey.into(),
249        });
250        self
251    }
252
253    /// Enables experimental engine features.
254    #[must_use]
255    pub fn experimental(mut self, experimental: Experimental) -> Self {
256        self.experimental = experimental;
257        self
258    }
259
260    /// Sets the maximum number of pooled connections (minimum 1, default 8).
261    #[must_use]
262    pub fn max_connections(mut self, max: usize) -> Self {
263        self.max_connections = max.max(1);
264        self
265    }
266
267    /// Sets how long to wait for a free pooled connection (default 30 s).
268    #[must_use]
269    pub fn acquire_timeout(mut self, timeout: Duration) -> Self {
270        self.acquire_timeout = timeout;
271        self
272    }
273
274    /// Sets how long a connection waits on a lock before failing with a busy
275    /// error (default 5 s); `None` fails immediately.
276    #[must_use]
277    pub fn busy_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
278        self.busy_timeout = timeout.into();
279        self
280    }
281
282    /// Enforces foreign keys (`PRAGMA foreign_keys = ON`, default on).
283    #[must_use]
284    pub fn foreign_keys(mut self, enabled: bool) -> Self {
285        self.foreign_keys = enabled;
286        self
287    }
288
289    /// Switches the database to MVCC (`PRAGMA journal_mode = 'mvcc'`), which
290    /// enables `BEGIN CONCURRENT` and multiple concurrent writers.
291    ///
292    /// Experimental in Turso; conflicts surface at commit as busy errors.
293    #[must_use]
294    pub fn mvcc(mut self, enabled: bool) -> Self {
295        self.mvcc = enabled;
296        self
297    }
298
299    /// Runs `PRAGMA name = value` on every new connection.
300    #[must_use]
301    pub fn pragma(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
302        self.pragmas.push((name.into(), value.into()));
303        self
304    }
305
306    /// The configured source.
307    pub fn source(&self) -> &Source {
308        &self.source
309    }
310
311    /// The configured pool size.
312    pub fn max_connections_value(&self) -> usize {
313        self.max_connections
314    }
315
316    /// The configured busy timeout.
317    pub fn busy_timeout_value(&self) -> Option<Duration> {
318        self.busy_timeout
319    }
320
321    /// Whether this points at the in-memory database.
322    pub fn is_in_memory(&self) -> bool {
323        matches!(self.source, Source::Memory)
324    }
325
326    /// Builds the engine builder for a local database at `path`, applying
327    /// the read-only, encryption and experimental settings.
328    pub(crate) fn local_builder(&self, path: &str) -> turso::Builder {
329        let mut builder = turso::Builder::new_local(path).read_only(self.read_only);
330        if let Some(enc) = &self.encryption {
331            builder =
332                builder
333                    .experimental_encryption(true)
334                    .with_encryption(turso::EncryptionOpts {
335                        cipher: enc.cipher.clone(),
336                        hexkey: enc.hexkey.clone(),
337                    });
338        }
339        let x = self.experimental;
340        builder
341            .experimental_attach(x.attach)
342            .experimental_custom_types(x.custom_types)
343            .experimental_generated_columns(x.generated_columns)
344            .experimental_index_method(x.index_method)
345            .experimental_materialized_views(x.materialized_views)
346            .experimental_vacuum(x.vacuum)
347            .experimental_multiprocess_wal(x.multiprocess_wal)
348            .experimental_without_rowid(x.without_rowid)
349    }
350}
351
352impl From<&str> for ConnectOptions {
353    fn from(path: &str) -> Self {
354        Self::new(path)
355    }
356}
357
358impl From<String> for ConnectOptions {
359    fn from(path: String) -> Self {
360        Self::new(path)
361    }
362}
363
364impl From<PathBuf> for ConnectOptions {
365    fn from(path: PathBuf) -> Self {
366        Self::new(path)
367    }
368}
369
370impl From<&Path> for ConnectOptions {
371    fn from(path: &Path) -> Self {
372        Self::new(path)
373    }
374}