qml_rs/storage/
database_init.rs1use crate::storage::error::StorageError;
7#[cfg(feature = "postgres")]
8use crate::storage::{PostgresConfig, PostgresStorage};
9#[cfg(feature = "postgres")]
10use std::time::Duration;
11
12#[cfg(feature = "postgres")]
14#[derive(Debug, Clone)]
15pub struct DatabaseInitializer {
16 config: PostgresConfig,
17 retry_attempts: u32,
18 retry_delay: Duration,
19 health_check_enabled: bool,
20}
21
22#[cfg(feature = "postgres")]
23impl DatabaseInitializer {
24 pub fn new() -> Self {
26 Self {
27 config: PostgresConfig::new(),
28 retry_attempts: 3,
29 retry_delay: Duration::from_secs(2),
30 health_check_enabled: true,
31 }
32 }
33
34 pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
36 self.config = self.config.with_database_url(url);
37 self
38 }
39
40 pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
42 self.config = self.config.with_auto_migrate(enabled);
43 self
44 }
45
46 pub fn with_pool_config(mut self, max_connections: u32, min_connections: u32) -> Self {
48 self.config = self
49 .config
50 .with_max_connections(max_connections)
51 .with_min_connections(min_connections);
52 self
53 }
54
55 pub fn with_retry_config(mut self, attempts: u32, delay: Duration) -> Self {
57 self.retry_attempts = attempts;
58 self.retry_delay = delay;
59 self
60 }
61
62 pub fn with_health_checks(mut self, enabled: bool) -> Self {
64 self.health_check_enabled = enabled;
65 self
66 }
67
68 pub async fn initialize(self) -> Result<PostgresStorage, DatabaseInitError> {
70 let mut last_error = None;
71
72 for attempt in 1..=self.retry_attempts {
73 tracing::info!(
74 "Database initialization attempt {} of {}",
75 attempt,
76 self.retry_attempts
77 );
78
79 match self.try_initialize().await {
80 Ok(storage) => {
81 tracing::info!("Database initialized successfully on attempt {}", attempt);
82
83 if self.health_check_enabled {
84 self.perform_health_check(&storage).await?;
85 }
86
87 return Ok(storage);
88 }
89 Err(e) => {
90 tracing::warn!("Initialization attempt {} failed: {}", attempt, e);
91 last_error = Some(e);
92
93 if attempt < self.retry_attempts {
94 tracing::info!("Retrying in {:?}...", self.retry_delay);
95 tokio::time::sleep(self.retry_delay).await;
96 }
97 }
98 }
99 }
100
101 Err(last_error.unwrap_or(DatabaseInitError::Unknown))
102 }
103
104 async fn try_initialize(&self) -> Result<PostgresStorage, DatabaseInitError> {
106 PostgresStorage::new(self.config.clone())
107 .await
108 .map_err(DatabaseInitError::StorageError)
109 }
110
111 async fn perform_health_check(
113 &self,
114 storage: &PostgresStorage,
115 ) -> Result<(), DatabaseInitError> {
116 tracing::debug!("Performing database health check...");
117
118 match storage.schema_exists().await {
120 Ok(true) => {
121 tracing::debug!("Schema health check passed");
122 }
123 Ok(false) => {
124 return Err(DatabaseInitError::HealthCheck(
125 "Schema does not exist after initialization".to_string(),
126 ));
127 }
128 Err(e) => {
129 return Err(DatabaseInitError::HealthCheck(format!(
130 "Schema check failed: {}",
131 e
132 )));
133 }
134 }
135
136 match storage.pool().acquire().await {
138 Ok(_) => {
139 tracing::debug!("Connectivity health check passed");
140 Ok(())
141 }
142 Err(e) => Err(DatabaseInitError::HealthCheck(format!(
143 "Connectivity check failed: {}",
144 e
145 ))),
146 }
147 }
148}
149
150#[cfg(feature = "postgres")]
151impl Default for DatabaseInitializer {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum DatabaseInitError {
160 #[error("Storage initialization failed: {0}")]
161 StorageError(#[from] StorageError),
162
163 #[error("Health check failed: {0}")]
164 HealthCheck(String),
165
166 #[error("Configuration error: {0}")]
167 Configuration(String),
168
169 #[error("Unknown initialization error")]
170 Unknown,
171}
172
173#[cfg(feature = "postgres")]
175pub mod quick_init {
176 use super::*;
177
178 pub async fn development(database_url: String) -> Result<PostgresStorage, DatabaseInitError> {
180 DatabaseInitializer::new()
181 .with_database_url(database_url)
182 .with_auto_migrate(true)
183 .with_pool_config(10, 1)
184 .initialize()
185 .await
186 }
187
188 pub async fn production(database_url: String) -> Result<PostgresStorage, DatabaseInitError> {
190 DatabaseInitializer::new()
191 .with_database_url(database_url)
192 .with_auto_migrate(false)
193 .with_pool_config(50, 5)
194 .with_retry_config(5, Duration::from_secs(3))
195 .initialize()
196 .await
197 }
198
199 pub async fn testing(database_url: String) -> Result<PostgresStorage, DatabaseInitError> {
201 DatabaseInitializer::new()
202 .with_database_url(database_url)
203 .with_auto_migrate(true)
204 .with_pool_config(2, 1)
205 .with_health_checks(false)
206 .initialize()
207 .await
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 #[cfg(feature = "postgres")]
214 use super::{DatabaseInitializer, Duration};
215
216 #[tokio::test]
217 #[cfg(feature = "postgres")]
218 async fn test_database_initializer_builder() {
219 let initializer = DatabaseInitializer::new()
220 .with_database_url("postgresql://test")
221 .with_auto_migrate(false)
222 .with_pool_config(5, 1)
223 .with_retry_config(2, Duration::from_millis(100));
224
225 assert_eq!(initializer.config.database_url, "postgresql://test");
226 assert!(!initializer.config.auto_migrate);
227 assert_eq!(initializer.config.max_connections, 5);
228 assert_eq!(initializer.retry_attempts, 2);
229 }
230}