use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Source {
Memory,
File(PathBuf),
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
Sync(SyncOptions),
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
Remote(RemoteOptions),
}
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RemoteOptions {
pub url: String,
pub auth_token: Option<String>,
pub remote_encryption_key: Option<String>,
}
#[cfg(feature = "serverless")]
impl fmt::Debug for RemoteOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RemoteOptions")
.field("url", &self.url)
.field("auth_token", &self.auth_token.as_ref().map(|_| Redacted))
.field(
"remote_encryption_key",
&self.remote_encryption_key.as_ref().map(|_| Redacted),
)
.finish()
}
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
#[derive(Clone, PartialEq, Eq)]
pub struct SyncOptions {
pub path: PathBuf,
pub remote_url: String,
pub auth_token: Option<String>,
pub bootstrap_if_empty: bool,
}
#[cfg(feature = "sync")]
impl fmt::Debug for SyncOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SyncOptions")
.field("path", &self.path)
.field("remote_url", &self.remote_url)
.field("auth_token", &self.auth_token.as_ref().map(|_| Redacted))
.field("bootstrap_if_empty", &self.bootstrap_if_empty)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Encryption {
pub cipher: String,
pub hexkey: String,
}
impl fmt::Debug for Encryption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Encryption")
.field("cipher", &self.cipher)
.field("hexkey", &Redacted)
.finish()
}
}
struct Redacted;
impl fmt::Debug for Redacted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<redacted>")
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
#[allow(
clippy::struct_excessive_bools,
reason = "the flags mirror the independent flags of turso::Builder"
)]
pub struct Experimental {
pub attach: bool,
pub custom_types: bool,
pub generated_columns: bool,
pub index_method: bool,
pub materialized_views: bool,
pub vacuum: bool,
pub multiprocess_wal: bool,
pub without_rowid: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConnectOptions {
pub(crate) source: Source,
pub(crate) read_only: bool,
pub(crate) encryption: Option<Encryption>,
pub(crate) experimental: Experimental,
pub(crate) max_connections: usize,
pub(crate) acquire_timeout: Duration,
pub(crate) busy_timeout: Option<Duration>,
pub(crate) foreign_keys: bool,
pub(crate) mvcc: bool,
pub(crate) pragmas: Vec<(String, String)>,
}
impl ConnectOptions {
fn with_source(source: Source) -> Self {
Self {
source,
read_only: false,
encryption: None,
experimental: Experimental::default(),
max_connections: 8,
acquire_timeout: Duration::from_secs(30),
busy_timeout: Some(Duration::from_secs(5)),
foreign_keys: true,
mvcc: false,
pragmas: Vec::new(),
}
}
pub fn new(path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
if path.as_os_str() == ":memory:" {
Self::in_memory()
} else {
Self::with_source(Source::File(path.to_path_buf()))
}
}
pub fn in_memory() -> Self {
Self::with_source(Source::Memory)
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub fn sync(path: impl AsRef<Path>, remote_url: impl Into<String>) -> Self {
Self::with_source(Source::Sync(SyncOptions {
path: path.as_ref().to_path_buf(),
remote_url: remote_url.into(),
auth_token: None,
bootstrap_if_empty: true,
}))
}
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
pub fn remote(url: impl Into<String>) -> Self {
Self::with_source(Source::Remote(RemoteOptions {
url: url.into(),
auth_token: None,
remote_encryption_key: None,
}))
}
#[cfg(any(feature = "sync", feature = "serverless"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "sync", feature = "serverless"))))]
#[must_use]
pub fn auth_token(mut self, token: impl Into<String>) -> Self {
match &mut self.source {
#[cfg(feature = "sync")]
Source::Sync(opts) => opts.auth_token = Some(token.into()),
#[cfg(feature = "serverless")]
Source::Remote(opts) => opts.auth_token = Some(token.into()),
_ => {}
}
self
}
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
#[must_use]
pub fn remote_encryption_key(mut self, key: impl Into<String>) -> Self {
if let Source::Remote(opts) = &mut self.source {
opts.remote_encryption_key = Some(key.into());
}
self
}
#[must_use]
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
#[must_use]
pub fn encryption(mut self, cipher: impl Into<String>, hexkey: impl Into<String>) -> Self {
self.encryption = Some(Encryption {
cipher: cipher.into(),
hexkey: hexkey.into(),
});
self
}
#[must_use]
pub fn experimental(mut self, experimental: Experimental) -> Self {
self.experimental = experimental;
self
}
#[must_use]
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max.max(1);
self
}
#[must_use]
pub fn acquire_timeout(mut self, timeout: Duration) -> Self {
self.acquire_timeout = timeout;
self
}
#[must_use]
pub fn busy_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
self.busy_timeout = timeout.into();
self
}
#[must_use]
pub fn foreign_keys(mut self, enabled: bool) -> Self {
self.foreign_keys = enabled;
self
}
#[must_use]
pub fn mvcc(mut self, enabled: bool) -> Self {
self.mvcc = enabled;
self
}
#[must_use]
pub fn pragma(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.pragmas.push((name.into(), value.into()));
self
}
pub fn source(&self) -> &Source {
&self.source
}
pub fn max_connections_value(&self) -> usize {
self.max_connections
}
pub fn busy_timeout_value(&self) -> Option<Duration> {
self.busy_timeout
}
pub fn is_in_memory(&self) -> bool {
matches!(self.source, Source::Memory)
}
#[allow(
clippy::unused_self,
reason = "the source is only inspected when the sync feature is on"
)]
pub(crate) fn replica_retry_budget(&self) -> Option<Duration> {
#[cfg(feature = "sync")]
if matches!(self.source, Source::Sync(_)) {
return Some(self.busy_timeout.unwrap_or_default());
}
None
}
pub(crate) fn local_builder(&self, path: &str) -> turso::Builder {
let mut builder = turso::Builder::new_local(path).read_only(self.read_only);
if let Some(enc) = &self.encryption {
builder =
builder
.experimental_encryption(true)
.with_encryption(turso::EncryptionOpts {
cipher: enc.cipher.clone(),
hexkey: enc.hexkey.clone(),
});
}
let x = self.experimental;
builder
.experimental_attach(x.attach)
.experimental_custom_types(x.custom_types)
.experimental_generated_columns(x.generated_columns)
.experimental_index_method(x.index_method)
.experimental_materialized_views(x.materialized_views)
.experimental_vacuum(x.vacuum)
.experimental_multiprocess_wal(x.multiprocess_wal)
.experimental_without_rowid(x.without_rowid)
}
}
impl From<&str> for ConnectOptions {
fn from(path: &str) -> Self {
Self::new(path)
}
}
impl From<String> for ConnectOptions {
fn from(path: String) -> Self {
Self::new(path)
}
}
impl From<PathBuf> for ConnectOptions {
fn from(path: PathBuf) -> Self {
Self::new(path)
}
}
impl From<&Path> for ConnectOptions {
fn from(path: &Path) -> Self {
Self::new(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_redacts_the_encryption_key() {
let opts = ConnectOptions::new("app.db").encryption("aes256gcm", "deadbeef");
let printed = format!("{opts:?}");
assert!(printed.contains("aes256gcm"));
assert!(printed.contains("<redacted>"));
assert!(!printed.contains("deadbeef"));
}
#[cfg(feature = "sync")]
#[test]
fn debug_redacts_the_replica_token() {
let opts = ConnectOptions::sync("replica.db", "turso://db.turso.io").auth_token("s3cret");
let printed = format!("{opts:?}");
assert!(printed.contains("turso://db.turso.io"));
assert!(printed.contains("Some(<redacted>)"));
assert!(!printed.contains("s3cret"));
}
#[cfg(feature = "serverless")]
#[test]
fn debug_redacts_the_remote_secrets() {
let opts = ConnectOptions::remote("turso://db.turso.io")
.auth_token("s3cret")
.remote_encryption_key("a2V5");
let printed = format!("{opts:?}");
assert!(printed.contains("turso://db.turso.io"));
assert!(!printed.contains("s3cret"));
assert!(!printed.contains("a2V5"));
}
}
#[cfg(all(test, feature = "sync"))]
mod replica_tests {
use super::*;
#[test]
fn replica_retry_budget_follows_the_source() {
let replica = ConnectOptions::sync("replica.db", "turso://db.turso.io");
assert_eq!(replica.replica_retry_budget(), Some(Duration::from_secs(5)));
let replica = ConnectOptions::sync("replica.db", "turso://db.turso.io").busy_timeout(None);
assert_eq!(replica.replica_retry_budget(), Some(Duration::ZERO));
assert_eq!(ConnectOptions::new("app.db").replica_retry_budget(), None);
}
}