mockforge_http/
database.rs1#[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#[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 #[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 #[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 #[cfg(feature = "database")]
54 pub async fn migrate_if_connected(&self) -> AnyhowResult<()> {
55 if let Some(ref pool) = self.pool {
56 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 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 #[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 #[cfg(feature = "database")]
94 pub fn pool(&self) -> Option<&PgPool> {
95 self.pool.as_deref()
96 }
97
98 #[cfg(not(feature = "database"))]
100 pub fn pool(&self) -> Option<()> {
101 None
102 }
103
104 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}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[tokio::test]
122 async fn test_database_connect_optional_none() {
123 let db = Database::connect_optional(None).await.unwrap();
124 assert!(!db.is_connected());
125 }
126
127 #[tokio::test]
128 async fn test_database_connect_optional_empty_string() {
129 let db = Database::connect_optional(Some("")).await.unwrap();
130 assert!(!db.is_connected());
131 }
132
133 #[tokio::test]
134 async fn test_database_pool_returns_none_when_not_connected() {
135 let db = Database::connect_optional(None).await.unwrap();
136 assert!(db.pool().is_none());
137 }
138
139 #[tokio::test]
140 async fn test_database_migrate_skips_when_not_connected() {
141 let db = Database::connect_optional(None).await.unwrap();
142 let result = db.migrate_if_connected().await;
144 assert!(result.is_ok());
145 }
146
147 #[test]
148 fn test_database_is_connected_returns_false_by_default() {
149 #[cfg(not(feature = "database"))]
151 {
152 let db = Database {
153 _phantom: std::marker::PhantomData,
154 };
155 assert!(!db.is_connected());
156 }
157 }
158
159 #[test]
160 fn test_database_clone() {
161 #[cfg(not(feature = "database"))]
163 {
164 let db = Database {
165 _phantom: std::marker::PhantomData,
166 };
167 let cloned = db.clone();
168 assert!(!cloned.is_connected());
169 }
170 }
171}