#![deny(unsafe_code)]
#![warn(missing_docs)]
use std::str::FromStr;
pub type DbConn = sqlx::SqlitePool;
pub async fn connect(url: &str) -> anyhow::Result<sqlx::SqlitePool> {
let opts = sqlx::sqlite::SqliteConnectOptions::from_str(url)?
.create_if_missing(true)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.foreign_keys(true);
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(8)
.connect_with(opts)
.await?;
tracing::info!("connected to SQLite database: {}", url);
Ok(pool)
}
pub async fn run_internal_migrations(pool: &sqlx::SqlitePool) -> anyhow::Result<()> {
let migrator = sqlx::migrate!("./migrations");
let num_migrations = migrator.migrations.len();
migrator.run(pool).await?;
tracing::info!(
count = num_migrations,
"applied atrg internal migrations (if pending)"
);
Ok(())
}
pub async fn run_user_migrations(
pool: &sqlx::SqlitePool,
dir: &std::path::Path,
) -> anyhow::Result<()> {
if !dir.exists() {
tracing::debug!(
path = %dir.display(),
"user migrations directory does not exist, skipping"
);
return Ok(());
}
let has_sql_files = std::fs::read_dir(dir)?
.filter_map(|entry| entry.ok())
.any(|entry| entry.path().extension().is_some_and(|ext| ext == "sql"));
if !has_sql_files {
tracing::debug!(
path = %dir.display(),
"user migrations directory contains no .sql files, skipping"
);
return Ok(());
}
let migrator = sqlx::migrate::Migrator::new(dir).await?;
let num_migrations = migrator.migrations.len();
migrator.run(pool).await?;
tracing::info!(
count = num_migrations,
path = %dir.display(),
"applied user migrations (if pending)"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_connect_memory() {
let pool = connect("sqlite::memory:")
.await
.expect("should connect to in-memory SQLite");
let row: (i32,) = sqlx::query_as("SELECT 1")
.fetch_one(&pool)
.await
.expect("should execute SELECT 1");
assert_eq!(row.0, 1);
}
#[tokio::test]
async fn test_internal_migrations() {
let pool = connect("sqlite::memory:").await.expect("should connect");
run_internal_migrations(&pool)
.await
.expect("should run internal migrations");
let row: (String,) = sqlx::query_as(
"SELECT name FROM sqlite_master WHERE type='table' AND name='atrg_sessions'",
)
.fetch_one(&pool)
.await
.expect("atrg_sessions table should exist");
assert_eq!(row.0, "atrg_sessions");
}
#[tokio::test]
async fn test_migrations_idempotent() {
let pool = connect("sqlite::memory:").await.expect("should connect");
run_internal_migrations(&pool)
.await
.expect("first run should succeed");
run_internal_migrations(&pool)
.await
.expect("second run should also succeed (idempotent)");
}
#[tokio::test]
async fn test_user_migrations_empty_dir() {
let pool = connect("sqlite::memory:").await.expect("should connect");
let tmp_dir = std::env::temp_dir().join(format!("atrg_test_empty_{}", std::process::id()));
std::fs::create_dir_all(&tmp_dir).expect("should create temp dir");
let result = run_user_migrations(&pool, &tmp_dir).await;
let _ = std::fs::remove_dir_all(&tmp_dir);
result.expect("empty dir should succeed silently");
}
#[tokio::test]
async fn test_user_migrations_nonexistent_dir() {
let pool = connect("sqlite::memory:").await.expect("should connect");
let nonexistent =
std::path::Path::new("/tmp/atrg_test_nonexistent_dir_that_does_not_exist");
run_user_migrations(&pool, nonexistent)
.await
.expect("nonexistent dir should succeed silently");
}
}