Skip to main content

dataprof_db/connectors/
mysql.rs

1//! MySQL/MariaDB database connector
2
3#[cfg(not(feature = "mysql"))]
4use super::common::feature_not_enabled_error;
5#[cfg(feature = "mysql")]
6use super::common::{build_count_query, not_connected_error};
7use crate::connection::ConnectionInfo;
8#[cfg(feature = "mysql")]
9use crate::security::validate_sql_identifier;
10use crate::{DataProfilerError, DatabaseConfig, DatabaseConnector, QueryColumns};
11#[cfg(feature = "mysql")]
12use crate::{process_rows_to_columns, streaming_profile_loop};
13use async_trait::async_trait;
14
15#[cfg(feature = "mysql")]
16use {sqlx::mysql::MySqlPool, sqlx::mysql::MySqlPoolOptions};
17
18/// MySQL/MariaDB connector
19pub struct MySqlConnector {
20    #[allow(dead_code)]
21    config: DatabaseConfig,
22    #[allow(dead_code)]
23    connection_info: ConnectionInfo,
24    #[cfg(feature = "mysql")]
25    pool: Option<MySqlPool>,
26    #[cfg(not(feature = "mysql"))]
27    #[allow(dead_code)]
28    pool: Option<()>,
29}
30
31impl MySqlConnector {
32    /// Create a new MySQL connector
33    pub fn new(config: DatabaseConfig) -> Result<Self, DataProfilerError> {
34        let connection_info = ConnectionInfo::parse(&config.connection_string)?;
35
36        if connection_info.database_type() != "mysql" {
37            return Err(DataProfilerError::DatabaseConfigError {
38                message: format!(
39                    "Invalid connection string for MySQL: expected a mysql:// URL, got a {} URL",
40                    connection_info.database_type()
41                ),
42            });
43        }
44
45        Ok(Self {
46            config,
47            connection_info,
48            pool: None,
49        })
50    }
51}
52
53#[async_trait]
54impl DatabaseConnector for MySqlConnector {
55    async fn connect(&mut self) -> Result<(), DataProfilerError> {
56        #[cfg(feature = "mysql")]
57        {
58            let connection_string = self.connection_info.to_connection_string("sqlx");
59
60            let pool = MySqlPoolOptions::new()
61                .max_connections(self.config.max_connections.unwrap_or(10))
62                .acquire_timeout(
63                    self.config
64                        .connection_timeout
65                        .unwrap_or(std::time::Duration::from_secs(30)),
66                )
67                .connect(&connection_string)
68                .await
69                .map_err(|e| {
70                    DataProfilerError::database_connection(&format!(
71                        "Failed to connect to MySQL: {}",
72                        e
73                    ))
74                })?;
75
76            self.pool = Some(pool);
77            Ok(())
78        }
79
80        #[cfg(not(feature = "mysql"))]
81        {
82            Err(DataProfilerError::database_feature_disabled(
83                "MySQL", "mysql",
84            ))
85        }
86    }
87
88    async fn disconnect(&mut self) -> Result<(), DataProfilerError> {
89        #[cfg(feature = "mysql")]
90        {
91            if let Some(pool) = &self.pool {
92                pool.close().await;
93                self.pool = None;
94            }
95        }
96        Ok(())
97    }
98
99    #[allow(unused_variables)]
100    async fn profile_query(&mut self, query: &str) -> Result<QueryColumns, DataProfilerError> {
101        #[cfg(feature = "mysql")]
102        {
103            let pool = self.pool.as_ref().ok_or_else(not_connected_error)?;
104
105            let rows = sqlx::query(query).fetch_all(pool).await.map_err(|e| {
106                DataProfilerError::database_query(&format!("Query execution failed: {}", e))
107            })?;
108
109            process_rows_to_columns!(rows)
110        }
111
112        #[cfg(not(feature = "mysql"))]
113        Err(feature_not_enabled_error("MySQL", "mysql"))
114    }
115
116    #[allow(unused_variables)]
117    async fn profile_query_streaming(
118        &mut self,
119        query: &str,
120        batch_size: usize,
121    ) -> Result<QueryColumns, DataProfilerError> {
122        #[cfg(feature = "mysql")]
123        {
124            let pool = self.pool.as_ref().ok_or_else(not_connected_error)?;
125
126            let count_query = build_count_query(query)?;
127            let total_rows: i64 = sqlx::query_scalar(&count_query)
128                .fetch_one(pool)
129                .await
130                .map_err(|e| {
131                    DataProfilerError::database_query(&format!("Failed to count rows: {}", e))
132                })?;
133
134            streaming_profile_loop!(pool, query, batch_size, total_rows, "MySQL")
135        }
136
137        #[cfg(not(feature = "mysql"))]
138        Err(feature_not_enabled_error("MySQL", "mysql"))
139    }
140
141    #[allow(unused_variables)]
142    async fn get_table_schema(
143        &mut self,
144        table_name: &str,
145    ) -> Result<Vec<String>, DataProfilerError> {
146        #[cfg(feature = "mysql")]
147        {
148            use sqlx::Row;
149
150            let pool = self.pool.as_ref().ok_or_else(not_connected_error)?;
151
152            let query = r#"
153                SELECT COLUMN_NAME
154                FROM INFORMATION_SCHEMA.COLUMNS
155                WHERE TABLE_NAME = ?
156                ORDER BY ORDINAL_POSITION
157            "#;
158
159            let rows = sqlx::query(query)
160                .bind(table_name)
161                .fetch_all(pool)
162                .await
163                .map_err(|e| {
164                    DataProfilerError::database_query(&format!("Failed to get table schema: {}", e))
165                })?;
166
167            let mut columns = Vec::new();
168            for row in rows {
169                let column_name: String = row.try_get(0).map_err(|e| {
170                    DataProfilerError::database_query(&format!("Failed to read column name: {}", e))
171                })?;
172                columns.push(column_name);
173            }
174
175            Ok(columns)
176        }
177
178        #[cfg(not(feature = "mysql"))]
179        Err(feature_not_enabled_error("MySQL", "mysql"))
180    }
181
182    #[allow(unused_variables)]
183    async fn count_table_rows(&mut self, table_name: &str) -> Result<u64, DataProfilerError> {
184        #[cfg(feature = "mysql")]
185        {
186            let pool = self.pool.as_ref().ok_or_else(not_connected_error)?;
187
188            validate_sql_identifier(table_name)?;
189            let query = format!("SELECT COUNT(*) FROM {}", table_name);
190            let count: i64 = sqlx::query_scalar(&query)
191                .fetch_one(pool)
192                .await
193                .map_err(|e| {
194                    DataProfilerError::database_query(&format!("Failed to count rows: {}", e))
195                })?;
196
197            Ok(count as u64)
198        }
199
200        #[cfg(not(feature = "mysql"))]
201        Err(feature_not_enabled_error("MySQL", "mysql"))
202    }
203
204    async fn test_connection(&mut self) -> Result<bool, DataProfilerError> {
205        #[cfg(feature = "mysql")]
206        {
207            let pool = self.pool.as_ref().ok_or_else(not_connected_error)?;
208
209            let result: i32 = sqlx::query_scalar("SELECT 1")
210                .fetch_one(pool)
211                .await
212                .map_err(|e| {
213                    DataProfilerError::database_query(&format!("Connection test failed: {}", e))
214                })?;
215
216            Ok(result == 1)
217        }
218
219        #[cfg(not(feature = "mysql"))]
220        Err(feature_not_enabled_error("MySQL", "mysql"))
221    }
222}