use crate::{TursoConnection, error::engine_error};
use log::LevelFilter;
use sqlx_core::{
connection::{ConnectOptions, LogSettings},
error::Error,
};
use std::{
path::{Path, PathBuf},
str::FromStr,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use url::Url;
#[derive(Debug, Clone)]
pub struct TursoConnectOptions {
pub(crate) path: Option<PathBuf>,
pub(crate) log_settings: LogSettings,
pub(crate) busy_timeout: Duration,
pub(crate) memory_pool_opened: Option<Arc<AtomicBool>>,
}
impl TursoConnectOptions {
pub fn busy_timeout(mut self, timeout: Duration) -> Self {
self.busy_timeout = timeout;
self
}
pub fn file(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
if path.as_os_str().is_empty() || path.to_str().is_none() {
return Err(Error::Configuration(
"Turso requires a nonempty UTF-8 file path".into(),
));
}
let path = if path.is_absolute() {
path.to_owned()
} else {
std::env::current_dir()?.join(path)
};
Ok(Self {
path: Some(path),
log_settings: LogSettings::default(),
busy_timeout: Duration::from_secs(1),
memory_pool_opened: None,
})
}
pub fn memory() -> Self {
Self {
path: None,
log_settings: LogSettings::default(),
busy_timeout: Duration::from_secs(1),
memory_pool_opened: None,
}
}
}
impl FromStr for TursoConnectOptions {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Error> {
Self::from_url(&Url::parse(value).map_err(|error| Error::Configuration(Box::new(error)))?)
}
}
impl ConnectOptions for TursoConnectOptions {
type Connection = TursoConnection;
fn from_url(url: &Url) -> Result<Self, Error> {
if url.query().is_some() || url.fragment().is_some() {
return Err(Error::Configuration(
"sqlx-turso: URL flags and fragments are unsupported".into(),
));
}
match url.scheme() {
"file" => {
if !matches!(url.host_str(), None | Some("localhost")) {
return Err(Error::Configuration(
"sqlx-turso: remote file URL authorities are unsupported".into(),
));
}
let path = url.to_file_path().map_err(|_| {
Error::Configuration("expected a local absolute file URL".into())
})?;
Self::file(path)
}
"turso-memory" if url.as_str() == "turso-memory:" => Ok(Self::memory()),
_ => Err(Error::Configuration(
"sqlx-turso accepts only local file:/// URLs or turso-memory:".into(),
)),
}
}
fn to_url_lossy(&self) -> Url {
match &self.path {
Some(path) => Url::from_file_path(path).expect("validated absolute file path"),
None => Url::parse("turso-memory:").expect("constant URL"),
}
}
async fn connect(&self) -> Result<TursoConnection, Error> {
if self
.memory_pool_opened
.as_ref()
.is_some_and(|opened| opened.swap(true, Ordering::AcqRel))
{
return Err(Error::Configuration(
"sqlx-turso: the memory pool connection was lost; create a new pool explicitly (memory data is not retained)".into(),
));
}
let canonical_path = self
.path
.as_deref()
.map(crate::migrate::canonical_data_path)
.transpose()?;
let path = match &canonical_path {
Some(path) => path
.to_str()
.ok_or_else(|| Error::Configuration("Turso requires a UTF-8 path".into()))?,
None => ":memory:",
};
let database = turso::Builder::new_local(path)
.build()
.await
.map_err(engine_error)?;
let inner = database.connect().map_err(engine_error)?;
inner
.busy_timeout(self.busy_timeout)
.map_err(engine_error)?;
Ok(TursoConnection {
inner,
_database: database,
log_settings: self.log_settings.clone(),
pending_statement: None,
unusable: false,
transaction_state: crate::transaction::TransactionState::Clean,
rejected_begin_drop: false,
migration_data_path: canonical_path,
migration_lock: None,
#[cfg(test)]
transition_hook: None,
#[cfg(test)]
rollback_fault: false,
#[cfg(test)]
migration_hook: None,
})
}
fn log_statements(mut self, level: LevelFilter) -> Self {
self.log_settings.log_statements(level);
self
}
fn log_slow_statements(mut self, level: LevelFilter, duration: Duration) -> Self {
self.log_settings.log_slow_statements(level, duration);
self
}
}