#[cfg(feature = "database")]
use anyhow::Result as AnyhowResult;
#[cfg(feature = "database")]
use sqlx::{postgres::PgPoolOptions, PgPool};
#[cfg(feature = "database")]
use std::sync::Arc;
#[derive(Clone)]
pub struct Database {
#[cfg(feature = "database")]
pool: Option<Arc<PgPool>>,
#[cfg(not(feature = "database"))]
_phantom: std::marker::PhantomData<()>,
}
impl Database {
#[cfg(feature = "database")]
pub const DEFAULT_MAX_CONNECTIONS: u32 = 10;
#[cfg(feature = "database")]
pub async fn connect_optional(database_url: Option<&str>) -> AnyhowResult<Self> {
Self::connect_optional_with_pool_size(database_url, None).await
}
#[cfg(feature = "database")]
pub async fn connect_optional_with_pool_size(
database_url: Option<&str>,
max_connections: Option<u32>,
) -> AnyhowResult<Self> {
let pool = if let Some(url) = database_url {
if url.is_empty() {
None
} else {
let max_conn = max_connections.unwrap_or_else(|| {
std::env::var("MOCKFORGE_DB_MAX_CONNECTIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(Self::DEFAULT_MAX_CONNECTIONS)
});
tracing::info!("Connecting to database with max_connections={}", max_conn);
let pool = PgPoolOptions::new().max_connections(max_conn).connect(url).await?;
Some(Arc::new(pool))
}
} else {
None
};
Ok(Self { pool })
}
#[cfg(not(feature = "database"))]
pub async fn connect_optional(_database_url: Option<&str>) -> anyhow::Result<Self> {
Ok(Self {
_phantom: std::marker::PhantomData,
})
}
#[cfg(feature = "database")]
pub async fn migrate_if_connected(&self) -> AnyhowResult<()> {
if let Some(ref pool) = self.pool {
match sqlx::migrate!("./migrations").run(pool.as_ref()).await {
Ok(_) => {
tracing::info!("Database migrations completed successfully");
Ok(())
}
Err(e) => {
if e.to_string().contains("previously applied but is missing") {
tracing::warn!(
"Migration tracking issue (manually applied migration): {:?}",
e
);
tracing::info!(
"Continuing despite migration tracking issue - database is up to date"
);
Ok(())
} else {
Err(e.into())
}
}
}
} else {
tracing::debug!("No database connection, skipping migrations");
Ok(())
}
}
#[cfg(not(feature = "database"))]
pub async fn migrate_if_connected(&self) -> anyhow::Result<()> {
tracing::debug!("Database feature not enabled, skipping migrations");
Ok(())
}
#[cfg(feature = "database")]
pub fn pool(&self) -> Option<&PgPool> {
self.pool.as_deref()
}
#[cfg(not(feature = "database"))]
pub fn pool(&self) -> Option<()> {
None
}
pub fn is_connected(&self) -> bool {
#[cfg(feature = "database")]
{
self.pool.is_some()
}
#[cfg(not(feature = "database"))]
{
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_database_connect_optional_none() {
let db = Database::connect_optional(None).await.unwrap();
assert!(!db.is_connected());
}
#[tokio::test]
async fn test_database_connect_optional_empty_string() {
let db = Database::connect_optional(Some("")).await.unwrap();
assert!(!db.is_connected());
}
#[tokio::test]
async fn test_database_pool_returns_none_when_not_connected() {
let db = Database::connect_optional(None).await.unwrap();
assert!(db.pool().is_none());
}
#[tokio::test]
async fn test_database_migrate_skips_when_not_connected() {
let db = Database::connect_optional(None).await.unwrap();
let result = db.migrate_if_connected().await;
assert!(result.is_ok());
}
#[test]
fn test_database_is_connected_returns_false_by_default() {
#[cfg(not(feature = "database"))]
{
let db = Database {
_phantom: std::marker::PhantomData,
};
assert!(!db.is_connected());
}
}
#[test]
fn test_database_clone() {
#[cfg(not(feature = "database"))]
{
let db = Database {
_phantom: std::marker::PhantomData,
};
let cloned = db.clone();
assert!(!cloned.is_connected());
}
}
}