dataprof_db/connectors/
mysql.rs1#[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
18pub 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 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}