1mod cell;
3mod config;
4#[cfg(feature = "health")]
5mod health;
6#[cfg(test)]
7mod live_tests;
8#[cfg(feature = "metrics")]
9mod metrics;
10mod pool;
11mod rdbms;
12mod routing;
13#[cfg(test)]
14mod tests;
15mod tracing;
18mod transaction;
19
20use async_trait::async_trait;
21use cell::{mysql_rows_to_result, pg_rows_to_result, sqlite_rows_to_result};
22use ecat_data::{
23 BackendKind, Dialect, RdbmsClient, RdbmsError, Row, SqlExecutor, run_with_timeout,
24};
25use std::time::Duration;
26
27pub use config::{MysqlCompat, PoolParams, SqlxConfig};
28#[cfg(feature = "health")]
29pub use health::RdbmsHealthCheck;
30#[cfg(feature = "metrics")]
31pub use metrics::register_pool_metrics;
32pub use pool::{Pool, PoolGuard};
33pub use routing::{SqlxRoutingConfig, build_routing};
34
35fn percent_encode(s: &str) -> String {
36 s.chars()
37 .map(|c| match c {
38 ':' | '/' | '@' | '#' | '?' | '&' | '=' | '%' | '+' | ' ' => {
39 format!("%{:02X}", c as u8)
40 }
41 _ => c.to_string(),
42 })
43 .collect()
44}
45
46fn with_auth_in_url(url: &str, username: &str, password: &str) -> String {
48 if url.contains('@') {
49 return url.to_string();
50 }
51 let encoded_user = percent_encode(username);
52 let encoded_pass = percent_encode(password);
53 url.replacen("://", &format!("://{encoded_user}:{encoded_pass}@"), 1)
54}
55
56fn db_err(e: sqlx::Error) -> RdbmsError {
63 #[cfg(feature = "metrics")]
64 if matches!(e, sqlx::Error::PoolTimedOut) {
65 metrics::count_pool_timeout();
66 }
67 RdbmsError::Database(e.to_string())
68}
69
70pub(crate) fn mysql_replica_status_sql(version: &str) -> &'static str {
73 let mut parts = version.split('.').map(|p| {
74 p.chars()
75 .take_while(char::is_ascii_digit)
76 .collect::<String>()
77 .parse::<u64>()
78 .unwrap_or(0)
79 });
80 let parsed = (
81 parts.next().unwrap_or(0),
82 parts.next().unwrap_or(0),
83 parts.next().unwrap_or(0),
84 );
85 if parsed >= (8, 0, 22) {
86 "SHOW REPLICA STATUS"
87 } else {
88 "SHOW SLAVE STATUS"
89 }
90}
91
92fn parse_mysql_replica_lag(rows: &[Row]) -> Option<Duration> {
95 let row = rows.first()?;
96 let seconds = row
97 .get("Seconds_Behind_Source")
98 .or_else(|| row.get("Seconds_Behind_Master"))?
99 .as_u64()?;
100 Some(Duration::from_secs(seconds))
101}
102
103pub struct SqlxClient {
104 pool: Pool,
105 query_timeout: Option<Duration>,
106 min_connections: u32,
107 slow_query: Option<Duration>,
110 version: std::sync::Mutex<Option<String>>,
112}
113
114impl SqlxClient {
115 pub async fn connect(url: &str) -> Result<Self, sqlx::Error> {
116 Self::connect_with_params(url, &PoolParams::for_url(url)).await
119 }
120
121 pub async fn connect_with_params(url: &str, params: &PoolParams) -> Result<Self, sqlx::Error> {
122 Self::connect_with_mysql_compat(url, params, MysqlCompat::Mysql).await
123 }
124
125 pub async fn connect_with_mysql_compat(
128 url: &str,
129 params: &PoolParams,
130 compat: MysqlCompat,
131 ) -> Result<Self, sqlx::Error> {
132 Ok(Self {
133 pool: Pool::connect_with_mysql_compat(url, params, compat).await?,
134 query_timeout: params.query_timeout,
135 min_connections: params.min_connections,
136 slow_query: params.slow_query,
137 version: std::sync::Mutex::new(None),
138 })
139 }
140
141 pub async fn connect_with_auth(
142 url: &str,
143 username: &str,
144 password: &str,
145 ) -> Result<Self, sqlx::Error> {
146 Self::connect(&with_auth_in_url(url, username, password)).await
147 }
148
149 pub async fn from_config(cfg: SqlxConfig) -> Result<Self, sqlx::Error> {
150 if cfg.tls.is_some() {
153 return Err(sqlx::Error::Configuration(Box::new(std::io::Error::new(
154 std::io::ErrorKind::InvalidInput,
155 "SqlxConfig.tls is not supported: configure TLS through URL parameters \
156 instead (e.g. postgres://...?sslmode=require)",
157 ))));
158 }
159 let params = cfg.pool();
160 let url = match (&cfg.username, &cfg.password) {
161 (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
162 with_auth_in_url(&cfg.url, u, p)
163 }
164 _ => cfg.url.clone(),
165 };
166 Self::connect_with_mysql_compat(&url, ¶ms, cfg.mysql_compat.unwrap_or_default()).await
167 }
168
169 pub fn from_pool(pool: Pool) -> Self {
176 Self {
177 pool,
178 query_timeout: None,
179 min_connections: 0,
180 slow_query: None,
181 version: std::sync::Mutex::new(None),
182 }
183 }
184
185 pub fn pool(&self) -> &Pool {
188 &self.pool
189 }
190
191 async fn timed<F, T>(&self, sql: &str, fut: F) -> Result<T, RdbmsError>
194 where
195 F: std::future::Future<Output = Result<T, RdbmsError>>,
196 {
197 crate::tracing::timed(
198 self.slow_query,
199 sql,
200 run_with_timeout(BackendKind::Rdbms, self.query_timeout, fut),
201 )
202 .await
203 }
204
205 async fn mysql_version(&self) -> Option<String> {
207 if let Some(v) = self.version.lock().unwrap().clone() {
208 return Some(v);
209 }
210 let rows = self.query("SELECT VERSION()").await.ok()?;
211 let v = rows.first()?.values().first()?.as_str()?.to_string();
212 *self.version.lock().unwrap() = Some(v.clone());
213 Some(v)
214 }
215
216 pub fn dialect(&self) -> Dialect {
217 self.pool.dialect()
218 }
219
220 pub fn pool_size(&self) -> u32 {
225 self.pool.size()
226 }
227
228 pub async fn warm_up(&self) -> Result<(), RdbmsError> {
235 let mut guards = Vec::with_capacity(self.min_connections as usize);
236 while (guards.len() as u32) < self.min_connections {
237 guards.push(
238 self.pool
239 .acquire()
240 .await
241 .map_err(|e| RdbmsError::Connection(e.to_string()))?,
242 );
243 }
244 Ok(())
246 }
247}
248
249#[async_trait]
250impl SqlExecutor for SqlxClient {
251 async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
252 self.timed(sql, async {
253 let affected = match &self.pool {
254 Pool::Pg(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
255 Pool::My(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
256 Pool::Sq(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
257 }
258 .map_err(db_err)?;
259 Ok(affected)
260 })
261 .await
262 }
263
264 async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
265 self.timed(sql, async {
266 let rows = match &self.pool {
267 Pool::Pg(p) => sqlx::query(sql)
268 .fetch_all(p)
269 .await
270 .map_err(db_err)
271 .and_then(pg_rows_to_result),
272 Pool::My(p) => sqlx::query(sql)
273 .fetch_all(p)
274 .await
275 .map_err(db_err)
276 .and_then(mysql_rows_to_result),
277 Pool::Sq(p) => sqlx::query(sql)
278 .fetch_all(p)
279 .await
280 .map_err(db_err)
281 .and_then(sqlite_rows_to_result),
282 }?;
283 Ok(rows)
284 })
285 .await
286 }
287
288 async fn execute_with(
289 &self,
290 sql: &str,
291 params: &[serde_json::Value],
292 ) -> Result<u64, RdbmsError> {
293 self.timed(sql, async {
294 let affected = match &self.pool {
295 Pool::Pg(p) => {
296 let mut q = sqlx::query(sql);
297 for param in params {
298 q = match param {
299 serde_json::Value::String(s) => q.bind(s.as_str()),
300 serde_json::Value::Number(n) => {
301 if let Some(i) = n.as_i64() {
302 q.bind(i)
303 } else if let Some(f) = n.as_f64() {
304 q.bind(f)
305 } else {
306 q.bind(n.to_string())
307 }
308 }
309 serde_json::Value::Bool(b) => q.bind(*b),
310 serde_json::Value::Null => q.bind(None::<String>),
311 _ => q.bind(param.to_string()),
312 };
313 }
314 q.execute(p).await.map(|r| r.rows_affected())
315 }
316 Pool::My(p) => {
317 let mut q = sqlx::query(sql);
318 for param in params {
319 q = match param {
320 serde_json::Value::String(s) => q.bind(s.as_str()),
321 serde_json::Value::Number(n) => {
322 if let Some(i) = n.as_i64() {
323 q.bind(i)
324 } else if let Some(f) = n.as_f64() {
325 q.bind(f)
326 } else {
327 q.bind(n.to_string())
328 }
329 }
330 serde_json::Value::Bool(b) => q.bind(*b),
331 serde_json::Value::Null => q.bind(None::<String>),
332 _ => q.bind(param.to_string()),
333 };
334 }
335 q.execute(p).await.map(|r| r.rows_affected())
336 }
337 Pool::Sq(p) => {
338 let mut q = sqlx::query(sql);
339 for param in params {
340 q = match param {
341 serde_json::Value::String(s) => q.bind(s.as_str()),
342 serde_json::Value::Number(n) => {
343 if let Some(i) = n.as_i64() {
344 q.bind(i)
345 } else if let Some(f) = n.as_f64() {
346 q.bind(f)
347 } else {
348 q.bind(n.to_string())
349 }
350 }
351 serde_json::Value::Bool(b) => q.bind(*b),
352 serde_json::Value::Null => q.bind(None::<String>),
353 _ => q.bind(param.to_string()),
354 };
355 }
356 q.execute(p).await.map(|r| r.rows_affected())
357 }
358 }
359 .map_err(db_err)?;
360 Ok(affected)
361 })
362 .await
363 }
364
365 async fn query_with(
366 &self,
367 sql: &str,
368 params: &[serde_json::Value],
369 ) -> Result<Vec<Row>, RdbmsError> {
370 self.timed(sql, async {
371 let rows = match &self.pool {
372 Pool::Pg(p) => {
373 let mut q = sqlx::query(sql);
374 for param in params {
375 q = match param {
376 serde_json::Value::String(s) => q.bind(s.as_str()),
377 serde_json::Value::Number(n) => {
378 if let Some(i) = n.as_i64() {
379 q.bind(i)
380 } else if let Some(f) = n.as_f64() {
381 q.bind(f)
382 } else {
383 q.bind(n.to_string())
384 }
385 }
386 serde_json::Value::Bool(b) => q.bind(*b),
387 serde_json::Value::Null => q.bind(None::<String>),
388 _ => q.bind(param.to_string()),
389 };
390 }
391 q.fetch_all(p)
392 .await
393 .map_err(db_err)
394 .and_then(pg_rows_to_result)
395 }
396 Pool::My(p) => {
397 let mut q = sqlx::query(sql);
398 for param in params {
399 q = match param {
400 serde_json::Value::String(s) => q.bind(s.as_str()),
401 serde_json::Value::Number(n) => {
402 if let Some(i) = n.as_i64() {
403 q.bind(i)
404 } else if let Some(f) = n.as_f64() {
405 q.bind(f)
406 } else {
407 q.bind(n.to_string())
408 }
409 }
410 serde_json::Value::Bool(b) => q.bind(*b),
411 serde_json::Value::Null => q.bind(None::<String>),
412 _ => q.bind(param.to_string()),
413 };
414 }
415 q.fetch_all(p)
416 .await
417 .map_err(db_err)
418 .and_then(mysql_rows_to_result)
419 }
420 Pool::Sq(p) => {
421 let mut q = sqlx::query(sql);
422 for param in params {
423 q = match param {
424 serde_json::Value::String(s) => q.bind(s.as_str()),
425 serde_json::Value::Number(n) => {
426 if let Some(i) = n.as_i64() {
427 q.bind(i)
428 } else if let Some(f) = n.as_f64() {
429 q.bind(f)
430 } else {
431 q.bind(n.to_string())
432 }
433 }
434 serde_json::Value::Bool(b) => q.bind(*b),
435 serde_json::Value::Null => q.bind(None::<String>),
436 _ => q.bind(param.to_string()),
437 };
438 }
439 q.fetch_all(p)
440 .await
441 .map_err(db_err)
442 .and_then(sqlite_rows_to_result)
443 }
444 }?;
445 Ok(rows)
446 })
447 .await
448 }
449
450 async fn execute_then_query(
456 &self,
457 first: &str,
458 first_params: &[serde_json::Value],
459 second: &str,
460 ) -> Result<Vec<Row>, RdbmsError> {
461 let tx = self.transaction().await?;
462 tx.execute_with(first, first_params).await?;
463 let rows = tx.query(second).await?;
464 tx.commit().await?;
465 Ok(rows)
466 }
467
468 fn dialect(&self) -> Dialect {
469 self.pool.dialect()
471 }
472}