Skip to main content

fraiseql_core/federation/
connection_manager.rs

1//! Connection management for direct database federation.
2//!
3//! Manages database connections to remote FraiseQL instances,
4//! enabling direct database queries without HTTP overhead.
5
6use std::{
7    collections::HashMap,
8    sync::{Arc, Mutex},
9};
10
11use crate::{
12    db::traits::DatabaseAdapter,
13    error::{FraiseQLError, Result},
14};
15
16/// Configuration for a remote database connection
17#[derive(Debug, Clone)]
18pub struct RemoteDatabaseConfig {
19    /// Connection string (e.g., "postgresql://user:pass@host:5432/dbname")
20    pub connection_string: String,
21    /// Optional pool size (default: 5)
22    pub pool_size:         Option<u32>,
23    /// Optional connection timeout in seconds (default: 5)
24    pub timeout_seconds:   Option<u32>,
25}
26
27impl RemoteDatabaseConfig {
28    /// Create a new remote database configuration
29    pub fn new(connection_string: impl Into<String>) -> Self {
30        Self {
31            connection_string: connection_string.into(),
32            pool_size:         None,
33            timeout_seconds:   None,
34        }
35    }
36
37    /// Set the connection pool size
38    pub fn with_pool_size(mut self, size: u32) -> Self {
39        self.pool_size = Some(size);
40        self
41    }
42
43    /// Set the connection timeout
44    pub fn with_timeout(mut self, seconds: u32) -> Self {
45        self.timeout_seconds = Some(seconds);
46        self
47    }
48
49    /// Get pool size (default 5)
50    pub fn get_pool_size(&self) -> u32 {
51        self.pool_size.unwrap_or(5)
52    }
53
54    /// Get timeout in seconds (default 5)
55    pub fn get_timeout_seconds(&self) -> u32 {
56        self.timeout_seconds.unwrap_or(5)
57    }
58}
59
60/// Manages connections to remote databases
61pub struct ConnectionManager {
62    /// Cached adapters keyed by connection string
63    adapters: Arc<Mutex<HashMap<String, Arc<dyn DatabaseAdapter>>>>,
64}
65
66impl ConnectionManager {
67    /// Create a new connection manager
68    pub fn new() -> Self {
69        Self {
70            adapters: Arc::new(Mutex::new(HashMap::new())),
71        }
72    }
73
74    /// Get or create a connection to a remote database
75    ///
76    /// # Arguments
77    ///
78    /// * `config` - Remote database configuration with connection string
79    ///
80    /// # Returns
81    ///
82    /// A database adapter for the remote connection
83    ///
84    /// # Errors
85    ///
86    /// Returns error if connection creation fails
87    pub async fn get_or_create_connection(
88        &self,
89        config: RemoteDatabaseConfig,
90    ) -> Result<Arc<dyn DatabaseAdapter>> {
91        // Check cache first
92        {
93            let adapters = self.adapters.lock().map_err(|e| FraiseQLError::Internal {
94                message: format!("Connection cache lock error: {}", e),
95                source:  None,
96            })?;
97
98            if let Some(adapter) = adapters.get(&config.connection_string) {
99                return Ok(Arc::clone(adapter));
100            }
101        }
102
103        // Create new connection
104        // Note: In production, this would create a real database adapter
105        // For now, we document the interface
106        Err(FraiseQLError::Internal {
107            message:
108                "Direct database connection creation requires database-specific implementation"
109                    .to_string(),
110            source:  None,
111        })
112    }
113
114    /// Close a specific connection by connection string
115    pub fn close_connection(&self, connection_string: &str) -> Result<()> {
116        let mut adapters = self.adapters.lock().map_err(|e| FraiseQLError::Internal {
117            message: format!("Connection cache lock error: {}", e),
118            source:  None,
119        })?;
120
121        adapters.remove(connection_string);
122        Ok(())
123    }
124
125    /// Close all cached connections
126    pub fn close_all(&self) -> Result<()> {
127        let mut adapters = self.adapters.lock().map_err(|e| FraiseQLError::Internal {
128            message: format!("Connection cache lock error: {}", e),
129            source:  None,
130        })?;
131
132        adapters.clear();
133        Ok(())
134    }
135
136    /// Get number of cached connections
137    pub fn connection_count(&self) -> Result<usize> {
138        let adapters = self.adapters.lock().map_err(|e| FraiseQLError::Internal {
139            message: format!("Connection cache lock error: {}", e),
140            source:  None,
141        })?;
142
143        Ok(adapters.len())
144    }
145}
146
147impl Default for ConnectionManager {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_remote_database_config_defaults() {
159        let config = RemoteDatabaseConfig::new("postgresql://localhost/db");
160        assert_eq!(config.get_pool_size(), 5);
161        assert_eq!(config.get_timeout_seconds(), 5);
162    }
163
164    #[test]
165    fn test_remote_database_config_custom() {
166        let config = RemoteDatabaseConfig::new("postgresql://localhost/db")
167            .with_pool_size(10)
168            .with_timeout(30);
169
170        assert_eq!(config.get_pool_size(), 10);
171        assert_eq!(config.get_timeout_seconds(), 30);
172    }
173
174    #[test]
175    fn test_connection_manager_creation() {
176        let _manager = ConnectionManager::new();
177        // Should not panic
178    }
179
180    #[test]
181    fn test_connection_manager_default() {
182        let _manager = ConnectionManager::default();
183        // Should not panic
184    }
185
186    #[test]
187    fn test_connection_count_empty() {
188        let manager = ConnectionManager::new();
189        assert_eq!(manager.connection_count().unwrap(), 0);
190    }
191
192    #[test]
193    fn test_close_all() {
194        let manager = ConnectionManager::new();
195        // Should not panic even with no connections
196        assert!(manager.close_all().is_ok());
197    }
198}