mockforge_http/
database.rs

1//! Database connection and migration support for mockforge-http
2//!
3//! This module provides optional database support for persistent storage
4//! of drift budgets, incidents, and consumer contracts.
5
6#[cfg(feature = "database")]
7use anyhow::Result as AnyhowResult;
8#[cfg(feature = "database")]
9use sqlx::{postgres::PgPoolOptions, PgPool};
10#[cfg(feature = "database")]
11use std::sync::Arc;
12
13/// Database connection wrapper
14#[derive(Clone)]
15pub struct Database {
16    #[cfg(feature = "database")]
17    pool: Option<Arc<PgPool>>,
18    #[cfg(not(feature = "database"))]
19    _phantom: std::marker::PhantomData<()>,
20}
21
22impl Database {
23    /// Create a new database connection (optional)
24    ///
25    /// If DATABASE_URL is not set or database feature is disabled,
26    /// returns a Database with no connection.
27    /// This allows the application to run without a database.
28    #[cfg(feature = "database")]
29    pub async fn connect_optional(database_url: Option<&str>) -> AnyhowResult<Self> {
30        let pool = if let Some(url) = database_url {
31            if url.is_empty() {
32                None
33            } else {
34                let pool = PgPoolOptions::new().max_connections(10).connect(url).await?;
35                Some(Arc::new(pool))
36            }
37        } else {
38            None
39        };
40
41        Ok(Self { pool })
42    }
43
44    /// Connect to database (no-op when database feature is disabled)
45    #[cfg(not(feature = "database"))]
46    pub async fn connect_optional(_database_url: Option<&str>) -> anyhow::Result<Self> {
47        Ok(Self {
48            _phantom: std::marker::PhantomData,
49        })
50    }
51
52    /// Run migrations if database is connected
53    #[cfg(feature = "database")]
54    pub async fn migrate_if_connected(&self) -> AnyhowResult<()> {
55        if let Some(ref pool) = self.pool {
56            // Run migrations from the migrations directory
57            // Note: This requires the migrations directory to be accessible at runtime
58            match sqlx::migrate!("./migrations").run(pool.as_ref()).await {
59                Ok(_) => {
60                    tracing::info!("Database migrations completed successfully");
61                    Ok(())
62                }
63                Err(e) => {
64                    // If migration was manually applied, log warning but continue
65                    if e.to_string().contains("previously applied but is missing") {
66                        tracing::warn!(
67                            "Migration tracking issue (manually applied migration): {:?}",
68                            e
69                        );
70                        tracing::info!(
71                            "Continuing despite migration tracking issue - database is up to date"
72                        );
73                        Ok(())
74                    } else {
75                        Err(e.into())
76                    }
77                }
78            }
79        } else {
80            tracing::debug!("No database connection, skipping migrations");
81            Ok(())
82        }
83    }
84
85    /// Run database migrations (no-op when database feature is disabled)
86    #[cfg(not(feature = "database"))]
87    pub async fn migrate_if_connected(&self) -> anyhow::Result<()> {
88        tracing::debug!("Database feature not enabled, skipping migrations");
89        Ok(())
90    }
91
92    /// Get the database pool if connected
93    #[cfg(feature = "database")]
94    pub fn pool(&self) -> Option<&PgPool> {
95        self.pool.as_deref()
96    }
97
98    /// Get the database pool (returns None when database feature is disabled)
99    #[cfg(not(feature = "database"))]
100    pub fn pool(&self) -> Option<()> {
101        None
102    }
103
104    /// Check if database is connected
105    pub fn is_connected(&self) -> bool {
106        #[cfg(feature = "database")]
107        {
108            self.pool.is_some()
109        }
110        #[cfg(not(feature = "database"))]
111        {
112            false
113        }
114    }
115}