Skip to main content

qml_rs/storage/
database_init.rs

1//! Database initialization utilities with automated migration support
2//!
3//! This module provides high-level utilities for initializing database
4//! connections with intelligent migration handling and error recovery.
5
6use crate::storage::error::StorageError;
7#[cfg(feature = "postgres")]
8use crate::storage::{PostgresConfig, PostgresStorage};
9#[cfg(feature = "postgres")]
10use std::time::Duration;
11
12/// Database initialization builder with best practices
13#[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    /// Create a new database initializer with sensible defaults
25    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    /// Set the database URL
35    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    /// Configure automatic migration behavior
41    pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
42        self.config = self.config.with_auto_migrate(enabled);
43        self
44    }
45
46    /// Configure connection pool settings
47    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    /// Configure retry behavior for connection attempts
56    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    /// Enable or disable health checks after initialization
63    pub fn with_health_checks(mut self, enabled: bool) -> Self {
64        self.health_check_enabled = enabled;
65        self
66    }
67
68    /// Initialize the database with retry logic and health checks
69    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    /// Single initialization attempt
105    async fn try_initialize(&self) -> Result<PostgresStorage, DatabaseInitError> {
106        PostgresStorage::new(self.config.clone())
107            .await
108            .map_err(DatabaseInitError::StorageError)
109    }
110
111    /// Perform post-initialization health checks
112    async fn perform_health_check(
113        &self,
114        storage: &PostgresStorage,
115    ) -> Result<(), DatabaseInitError> {
116        tracing::debug!("Performing database health check...");
117
118        // Check schema existence
119        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        // Test basic connectivity with a simple query
137        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/// Errors that can occur during database initialization
158#[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/// Convenience functions for common initialization patterns
174#[cfg(feature = "postgres")]
175pub mod quick_init {
176    use super::*;
177
178    /// Quick development setup with auto-migration
179    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    /// Production setup with manual migration control
189    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    /// Testing setup with minimal resources
200    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}