1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Database connection and migration support for mockforge-http
//!
//! This module provides optional database support for persistent storage
//! of drift budgets, incidents, and consumer contracts.
#[cfg(feature = "database")]
use anyhow::Result as AnyhowResult;
#[cfg(feature = "database")]
use sqlx::{postgres::PgPoolOptions, PgPool};
#[cfg(feature = "database")]
use std::sync::Arc;
/// Database connection wrapper
#[derive(Clone)]
pub struct Database {
#[cfg(feature = "database")]
pool: Option<Arc<PgPool>>,
#[cfg(not(feature = "database"))]
_phantom: std::marker::PhantomData<()>,
}
impl Database {
/// Default maximum database connections
#[cfg(feature = "database")]
pub const DEFAULT_MAX_CONNECTIONS: u32 = 10;
/// Create a new database connection (optional)
///
/// If DATABASE_URL is not set or database feature is disabled,
/// returns a Database with no connection.
/// This allows the application to run without a database.
///
/// The max_connections parameter defaults to 10 if not specified.
/// Can be configured via the MOCKFORGE_DB_MAX_CONNECTIONS environment variable.
#[cfg(feature = "database")]
pub async fn connect_optional(database_url: Option<&str>) -> AnyhowResult<Self> {
Self::connect_optional_with_pool_size(database_url, None).await
}
/// Create a new database connection with configurable pool size
///
/// If max_connections is None, uses MOCKFORGE_DB_MAX_CONNECTIONS env var
/// or defaults to DEFAULT_MAX_CONNECTIONS (10).
#[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 })
}
/// Connect to database (no-op when database feature is disabled)
#[cfg(not(feature = "database"))]
pub async fn connect_optional(_database_url: Option<&str>) -> anyhow::Result<Self> {
Ok(Self {
_phantom: std::marker::PhantomData,
})
}
/// Run migrations if database is connected
#[cfg(feature = "database")]
pub async fn migrate_if_connected(&self) -> AnyhowResult<()> {
if let Some(ref pool) = self.pool {
// Migrations live in `mockforge-http/migrations/` (per the migration
// guard's "append-only, no renames" policy — sqlx records each
// migration's checksum + version on apply, and renaming the file is
// indistinguishable from a checksum mismatch). `sqlx::migrate!` is a
// compile-time macro that bakes the SQL content into the binary, so
// the relative path here is resolved at build time against
// `CARGO_MANIFEST_DIR`; runtime location of the SQL files doesn't
// matter. Issue #555 prereq.
match sqlx::migrate!("../mockforge-http/migrations").run(pool.as_ref()).await {
Ok(_) => {
tracing::info!("Database migrations completed successfully");
Ok(())
}
Err(e) => {
// If migration was manually applied, log warning but continue
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(())
}
}
/// Run database migrations (no-op when database feature is disabled)
#[cfg(not(feature = "database"))]
pub async fn migrate_if_connected(&self) -> anyhow::Result<()> {
tracing::debug!("Database feature not enabled, skipping migrations");
Ok(())
}
/// Get the database pool if connected
#[cfg(feature = "database")]
pub fn pool(&self) -> Option<&PgPool> {
self.pool.as_deref()
}
/// Get the database pool (returns None when database feature is disabled)
#[cfg(not(feature = "database"))]
pub fn pool(&self) -> Option<()> {
None
}
/// Check if database is connected
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();
// Should succeed even without a connection
let result = db.migrate_if_connected().await;
assert!(result.is_ok());
}
#[test]
fn test_database_is_connected_returns_false_by_default() {
// Without database feature, is_connected always returns false
#[cfg(not(feature = "database"))]
{
let db = Database {
_phantom: std::marker::PhantomData,
};
assert!(!db.is_connected());
}
}
#[test]
fn test_database_clone() {
// Database should be Clone
#[cfg(not(feature = "database"))]
{
let db = Database {
_phantom: std::marker::PhantomData,
};
let cloned = db.clone();
assert!(!cloned.is_connected());
}
}
}