sqlx-turso-driver 0.0.1

An asynchronous SQLx driver for embedded Turso databases
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;

/// Local options preserve a filesystem path rather than routing it through a URL.
///
/// URL entry points accept only absolute `file:///...` URLs (empty authority or
/// `localhost`) without flags and `turso-memory:`. Remote authorities, query
/// options, and fragments are rejected on every platform.
/// Turso's builder takes UTF-8, so non-UTF-8 paths are rejected, never replaced.
#[derive(Debug, Clone)]
pub struct TursoConnectOptions {
    pub(crate) path: Option<PathBuf>,
    pub(crate) log_settings: LogSettings,
    pub(crate) busy_timeout: Duration,
    // Shared only by options belonging to a controlled memory pool, not by
    // ordinary memory options: independent direct connections remain supported.
    pub(crate) memory_pool_opened: Option<Arc<AtomicBool>>,
}
impl TursoConnectOptions {
    /// Set the engine's finite accumulated busy-wait budget (default: 1 second).
    /// Zero disables waiting; the original engine busy error is preserved.
    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(),
            ));
        }
        // Absolute paths prevent the engine's reserved ':memory:' name from
        // turning a requested file into an ephemeral database.
        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" => {
                // Enforce local-only policy before conversion: Windows otherwise
                // accepts remote authorities as UNC paths.
                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
    }
}