mod log;
mod schema;
use std::io;
use std::sync::Arc;
use std::time::Duration;
use sqlx::AnyPool;
use sqlx::any::AnyPoolOptions;
use turbojet::store::{Job, Opened, SessionId, SessionStorage};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SqlConfig {
pub lease: Duration,
pub holder: String,
pub max_session_bytes: u64,
pub sync: bool,
pub max_connections: u32,
}
impl SqlConfig {
pub const DEFAULT_MAX_SESSION_BYTES: u64 = 1 << 30;
pub const DEFAULT_LEASE: Duration = Duration::from_secs(120);
}
impl Default for SqlConfig {
fn default() -> Self {
Self {
lease: Self::DEFAULT_LEASE,
holder: default_holder(),
max_session_bytes: Self::DEFAULT_MAX_SESSION_BYTES,
sync: true,
max_connections: 8,
}
}
}
#[derive(Clone)]
pub struct SqlStorage {
shared: Arc<Shared>,
}
impl std::fmt::Debug for SqlStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SqlStorage").finish_non_exhaustive()
}
}
struct Shared {
pool: AnyPool,
config: SqlConfig,
backend: Backend,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Backend {
Sqlite,
Postgres,
}
impl SqlStorage {
pub async fn connect(url: &str, config: SqlConfig) -> io::Result<Self> {
let backend = Backend::of(url)?;
sqlx::any::install_default_drivers();
let sync = config.sync;
let pool = AnyPoolOptions::new()
.max_connections(config.max_connections)
.after_connect(move |connection, _| {
Box::pin(async move {
if backend == Backend::Sqlite {
schema::configure_sqlite(connection, sync).await?;
}
Ok(())
})
})
.connect(url)
.await
.map_err(log::database("connecting"))?;
Ok(Self { shared: Arc::new(Shared { pool, config, backend }) })
}
pub async fn migrate(&self) -> io::Result<()> {
schema::migrate(&self.shared.pool, self.shared.backend).await
}
}
impl SessionStorage for SqlStorage {
fn begin_open(&self, id: &SessionId) -> io::Result<Opened> {
Ok(Opened::Pending(Job::future(log::open(self.shared.clone(), id.clone()))))
}
}
impl Backend {
fn of(url: &str) -> io::Result<Self> {
let scheme = url.split_once(':').map_or(url, |(scheme, _)| scheme);
match scheme {
"sqlite" if cfg!(feature = "sqlite") => Ok(Self::Sqlite),
"postgres" | "postgresql" if cfg!(feature = "postgres") => Ok(Self::Postgres),
_ => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("turbojet-sql doesn't support '{scheme}' databases with the features it was built with"),
)),
}
}
}
fn default_holder() -> String {
let host = std::env::var("HOSTNAME").ok().filter(|h| !h.is_empty()).unwrap_or_else(|| "unknown-host".into());
let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
format!("{host}:{}:{started}", std::process::id())
}