use sqlx::SqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr as _;
pub static MIGRATIONS: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");
#[derive(Debug, thiserror::Error)]
#[expect(
clippy::module_name_repetitions,
reason = "`DbError` is the conventional name and `Error` would clash with the crate's other top-level error type"
)]
pub enum DbError {
#[error("invalid database URL: {0}")]
BadUrl(#[source] sqlx::Error),
#[error("failed to open SQLite pool: {0}")]
Open(#[source] sqlx::Error),
#[error("failed to apply migrations: {0}")]
Migrate(#[source] sqlx::migrate::MigrateError),
}
pub async fn open_and_migrate(url: &str) -> Result<SqlitePool, DbError> {
let migration_options = SqliteConnectOptions::from_str(url)
.map_err(DbError::BadUrl)?
.foreign_keys(false)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.busy_timeout(std::time::Duration::from_secs(5));
let migration_pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(migration_options)
.await
.map_err(DbError::Open)?;
MIGRATIONS
.run(&migration_pool)
.await
.map_err(DbError::Migrate)?;
let violations: Vec<(String, i64, String, i64)> = sqlx::query_as("PRAGMA foreign_key_check")
.fetch_all(&migration_pool)
.await
.map_err(|err| DbError::Migrate(sqlx::migrate::MigrateError::Execute(err)))?;
if !violations.is_empty() {
tracing::error!(
"post-migration foreign_key_check found {} violation(s): {:?}",
violations.len(),
violations
);
return Err(DbError::Migrate(sqlx::migrate::MigrateError::Execute(
sqlx::Error::Protocol(format!(
"post-migration foreign_key_check found {} violation(s)",
violations.len()
)),
)));
}
migration_pool.close().await;
let options = SqliteConnectOptions::from_str(url)
.map_err(DbError::BadUrl)?
.foreign_keys(true)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.busy_timeout(std::time::Duration::from_secs(5));
let pool = SqlitePoolOptions::new()
.max_connections(8)
.connect_with(options)
.await
.map_err(DbError::Open)?;
Ok(pool)
}