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::{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 Ok(Self {
123 pool: Pool::connect(url, params).await?,
124 query_timeout: params.query_timeout,
125 min_connections: params.min_connections,
126 slow_query: params.slow_query,
127 version: std::sync::Mutex::new(None),
128 })
129 }
130
131 pub async fn connect_with_auth(
132 url: &str,
133 username: &str,
134 password: &str,
135 ) -> Result<Self, sqlx::Error> {
136 Self::connect(&with_auth_in_url(url, username, password)).await
137 }
138
139 pub async fn from_config(cfg: SqlxConfig) -> Result<Self, sqlx::Error> {
140 if cfg.tls.is_some() {
143 return Err(sqlx::Error::Configuration(Box::new(std::io::Error::new(
144 std::io::ErrorKind::InvalidInput,
145 "SqlxConfig.tls is not supported: configure TLS through URL parameters \
146 instead (e.g. postgres://...?sslmode=require)",
147 ))));
148 }
149 let params = cfg.pool();
150 let url = match (&cfg.username, &cfg.password) {
151 (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
152 with_auth_in_url(&cfg.url, u, p)
153 }
154 _ => cfg.url.clone(),
155 };
156 Self::connect_with_params(&url, ¶ms).await
157 }
158
159 pub fn from_pool(pool: Pool) -> Self {
166 Self {
167 pool,
168 query_timeout: None,
169 min_connections: 0,
170 slow_query: None,
171 version: std::sync::Mutex::new(None),
172 }
173 }
174
175 pub fn pool(&self) -> &Pool {
178 &self.pool
179 }
180
181 async fn timed<F, T>(&self, sql: &str, fut: F) -> Result<T, RdbmsError>
184 where
185 F: std::future::Future<Output = Result<T, RdbmsError>>,
186 {
187 crate::tracing::timed(
188 self.slow_query,
189 sql,
190 run_with_timeout(BackendKind::Rdbms, self.query_timeout, fut),
191 )
192 .await
193 }
194
195 async fn mysql_version(&self) -> Option<String> {
197 if let Some(v) = self.version.lock().unwrap().clone() {
198 return Some(v);
199 }
200 let rows = self.query("SELECT VERSION()").await.ok()?;
201 let v = rows.first()?.values().first()?.as_str()?.to_string();
202 *self.version.lock().unwrap() = Some(v.clone());
203 Some(v)
204 }
205
206 pub fn dialect(&self) -> Dialect {
207 self.pool.dialect()
208 }
209
210 pub fn pool_size(&self) -> u32 {
215 self.pool.size()
216 }
217
218 pub async fn warm_up(&self) -> Result<(), RdbmsError> {
225 let mut guards = Vec::with_capacity(self.min_connections as usize);
226 while (guards.len() as u32) < self.min_connections {
227 guards.push(
228 self.pool
229 .acquire()
230 .await
231 .map_err(|e| RdbmsError::Connection(e.to_string()))?,
232 );
233 }
234 Ok(())
236 }
237}
238
239#[async_trait]
240impl SqlExecutor for SqlxClient {
241 async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
242 self.timed(sql, async {
243 let affected = match &self.pool {
244 Pool::Pg(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
245 Pool::My(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
246 Pool::Sq(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
247 }
248 .map_err(db_err)?;
249 Ok(affected)
250 })
251 .await
252 }
253
254 async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
255 self.timed(sql, async {
256 let rows = match &self.pool {
257 Pool::Pg(p) => sqlx::query(sql)
258 .fetch_all(p)
259 .await
260 .map_err(db_err)
261 .and_then(pg_rows_to_result),
262 Pool::My(p) => sqlx::query(sql)
263 .fetch_all(p)
264 .await
265 .map_err(db_err)
266 .and_then(mysql_rows_to_result),
267 Pool::Sq(p) => sqlx::query(sql)
268 .fetch_all(p)
269 .await
270 .map_err(db_err)
271 .and_then(sqlite_rows_to_result),
272 }?;
273 Ok(rows)
274 })
275 .await
276 }
277
278 async fn execute_with(
279 &self,
280 sql: &str,
281 params: &[serde_json::Value],
282 ) -> Result<u64, RdbmsError> {
283 self.timed(sql, async {
284 let affected = match &self.pool {
285 Pool::Pg(p) => {
286 let mut q = sqlx::query(sql);
287 for param in params {
288 q = match param {
289 serde_json::Value::String(s) => q.bind(s.as_str()),
290 serde_json::Value::Number(n) => {
291 if let Some(i) = n.as_i64() {
292 q.bind(i)
293 } else if let Some(f) = n.as_f64() {
294 q.bind(f)
295 } else {
296 q.bind(n.to_string())
297 }
298 }
299 serde_json::Value::Bool(b) => q.bind(*b),
300 serde_json::Value::Null => q.bind(None::<String>),
301 _ => q.bind(param.to_string()),
302 };
303 }
304 q.execute(p).await.map(|r| r.rows_affected())
305 }
306 Pool::My(p) => {
307 let mut q = sqlx::query(sql);
308 for param in params {
309 q = match param {
310 serde_json::Value::String(s) => q.bind(s.as_str()),
311 serde_json::Value::Number(n) => {
312 if let Some(i) = n.as_i64() {
313 q.bind(i)
314 } else if let Some(f) = n.as_f64() {
315 q.bind(f)
316 } else {
317 q.bind(n.to_string())
318 }
319 }
320 serde_json::Value::Bool(b) => q.bind(*b),
321 serde_json::Value::Null => q.bind(None::<String>),
322 _ => q.bind(param.to_string()),
323 };
324 }
325 q.execute(p).await.map(|r| r.rows_affected())
326 }
327 Pool::Sq(p) => {
328 let mut q = sqlx::query(sql);
329 for param in params {
330 q = match param {
331 serde_json::Value::String(s) => q.bind(s.as_str()),
332 serde_json::Value::Number(n) => {
333 if let Some(i) = n.as_i64() {
334 q.bind(i)
335 } else if let Some(f) = n.as_f64() {
336 q.bind(f)
337 } else {
338 q.bind(n.to_string())
339 }
340 }
341 serde_json::Value::Bool(b) => q.bind(*b),
342 serde_json::Value::Null => q.bind(None::<String>),
343 _ => q.bind(param.to_string()),
344 };
345 }
346 q.execute(p).await.map(|r| r.rows_affected())
347 }
348 }
349 .map_err(db_err)?;
350 Ok(affected)
351 })
352 .await
353 }
354
355 async fn query_with(
356 &self,
357 sql: &str,
358 params: &[serde_json::Value],
359 ) -> Result<Vec<Row>, RdbmsError> {
360 self.timed(sql, async {
361 let rows = match &self.pool {
362 Pool::Pg(p) => {
363 let mut q = sqlx::query(sql);
364 for param in params {
365 q = match param {
366 serde_json::Value::String(s) => q.bind(s.as_str()),
367 serde_json::Value::Number(n) => {
368 if let Some(i) = n.as_i64() {
369 q.bind(i)
370 } else if let Some(f) = n.as_f64() {
371 q.bind(f)
372 } else {
373 q.bind(n.to_string())
374 }
375 }
376 serde_json::Value::Bool(b) => q.bind(*b),
377 serde_json::Value::Null => q.bind(None::<String>),
378 _ => q.bind(param.to_string()),
379 };
380 }
381 q.fetch_all(p)
382 .await
383 .map_err(db_err)
384 .and_then(pg_rows_to_result)
385 }
386 Pool::My(p) => {
387 let mut q = sqlx::query(sql);
388 for param in params {
389 q = match param {
390 serde_json::Value::String(s) => q.bind(s.as_str()),
391 serde_json::Value::Number(n) => {
392 if let Some(i) = n.as_i64() {
393 q.bind(i)
394 } else if let Some(f) = n.as_f64() {
395 q.bind(f)
396 } else {
397 q.bind(n.to_string())
398 }
399 }
400 serde_json::Value::Bool(b) => q.bind(*b),
401 serde_json::Value::Null => q.bind(None::<String>),
402 _ => q.bind(param.to_string()),
403 };
404 }
405 q.fetch_all(p)
406 .await
407 .map_err(db_err)
408 .and_then(mysql_rows_to_result)
409 }
410 Pool::Sq(p) => {
411 let mut q = sqlx::query(sql);
412 for param in params {
413 q = match param {
414 serde_json::Value::String(s) => q.bind(s.as_str()),
415 serde_json::Value::Number(n) => {
416 if let Some(i) = n.as_i64() {
417 q.bind(i)
418 } else if let Some(f) = n.as_f64() {
419 q.bind(f)
420 } else {
421 q.bind(n.to_string())
422 }
423 }
424 serde_json::Value::Bool(b) => q.bind(*b),
425 serde_json::Value::Null => q.bind(None::<String>),
426 _ => q.bind(param.to_string()),
427 };
428 }
429 q.fetch_all(p)
430 .await
431 .map_err(db_err)
432 .and_then(sqlite_rows_to_result)
433 }
434 }?;
435 Ok(rows)
436 })
437 .await
438 }
439
440 async fn execute_then_query(
446 &self,
447 first: &str,
448 first_params: &[serde_json::Value],
449 second: &str,
450 ) -> Result<Vec<Row>, RdbmsError> {
451 let tx = self.transaction().await?;
452 tx.execute_with(first, first_params).await?;
453 let rows = tx.query(second).await?;
454 tx.commit().await?;
455 Ok(rows)
456 }
457
458 fn dialect(&self) -> Dialect {
459 self.pool.dialect()
461 }
462}