1mod cell;
3mod config;
4#[cfg(feature = "health")]
5mod health;
6#[cfg(test)]
7mod live_tests;
8#[cfg(feature = "metrics")]
9mod metrics;
10mod pool;
11#[cfg(test)]
12mod tests;
13mod tracing;
16mod transaction;
17
18use async_trait::async_trait;
19use cell::{mysql_rows_to_result, pg_rows_to_result, sqlite_rows_to_result};
20use ecat_data::{
21 Dialect, RdbmsClient, RdbmsError, Row, SqlExecutor, Transaction, TransactionInner,
22 run_with_timeout,
23};
24use std::time::Duration;
25use transaction::{MyTx, PgTx, SqTx};
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};
33
34fn percent_encode(s: &str) -> String {
35 s.chars()
36 .map(|c| match c {
37 ':' | '/' | '@' | '#' | '?' | '&' | '=' | '%' | '+' | ' ' => {
38 format!("%{:02X}", c as u8)
39 }
40 _ => c.to_string(),
41 })
42 .collect()
43}
44
45fn with_auth_in_url(url: &str, username: &str, password: &str) -> String {
47 if url.contains('@') {
48 return url.to_string();
49 }
50 let encoded_user = percent_encode(username);
51 let encoded_pass = percent_encode(password);
52 url.replacen("://", &format!("://{encoded_user}:{encoded_pass}@"), 1)
53}
54
55fn db_err(e: sqlx::Error) -> RdbmsError {
62 #[cfg(feature = "metrics")]
63 if matches!(e, sqlx::Error::PoolTimedOut) {
64 metrics::count_pool_timeout();
65 }
66 RdbmsError::Database(e.to_string())
67}
68
69pub struct SqlxClient {
70 pool: Pool,
71 query_timeout: Option<Duration>,
72 min_connections: u32,
73 slow_query: Option<Duration>,
76}
77
78impl SqlxClient {
79 pub async fn connect(url: &str) -> Result<Self, sqlx::Error> {
80 Self::connect_with_params(url, &PoolParams::for_url(url)).await
83 }
84
85 pub async fn connect_with_params(url: &str, params: &PoolParams) -> Result<Self, sqlx::Error> {
86 Ok(Self {
87 pool: Pool::connect(url, params).await?,
88 query_timeout: params.query_timeout,
89 min_connections: params.min_connections,
90 slow_query: params.slow_query,
91 })
92 }
93
94 pub async fn connect_with_auth(
95 url: &str,
96 username: &str,
97 password: &str,
98 ) -> Result<Self, sqlx::Error> {
99 Self::connect(&with_auth_in_url(url, username, password)).await
100 }
101
102 pub async fn from_config(cfg: SqlxConfig) -> Result<Self, sqlx::Error> {
103 if cfg.tls.is_some() {
106 return Err(sqlx::Error::Configuration(Box::new(std::io::Error::new(
107 std::io::ErrorKind::InvalidInput,
108 "SqlxConfig.tls is not supported: configure TLS through URL parameters \
109 instead (e.g. postgres://...?sslmode=require)",
110 ))));
111 }
112 let params = cfg.pool();
113 let url = match (&cfg.username, &cfg.password) {
114 (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
115 with_auth_in_url(&cfg.url, u, p)
116 }
117 _ => cfg.url.clone(),
118 };
119 Self::connect_with_params(&url, ¶ms).await
120 }
121
122 pub fn from_pool(pool: Pool) -> Self {
129 Self {
130 pool,
131 query_timeout: None,
132 min_connections: 0,
133 slow_query: None,
134 }
135 }
136
137 pub fn pool(&self) -> &Pool {
140 &self.pool
141 }
142
143 async fn timed<F, T>(&self, sql: &str, fut: F) -> Result<T, RdbmsError>
146 where
147 F: std::future::Future<Output = Result<T, RdbmsError>>,
148 {
149 crate::tracing::timed(
150 self.slow_query,
151 sql,
152 run_with_timeout(self.query_timeout, fut),
153 )
154 .await
155 }
156
157 pub fn dialect(&self) -> Dialect {
158 self.pool.dialect()
159 }
160
161 pub fn pool_size(&self) -> u32 {
163 self.pool.size()
164 }
165
166 pub async fn warm_up(&self) -> Result<(), RdbmsError> {
173 let mut guards = Vec::with_capacity(self.min_connections as usize);
174 while (guards.len() as u32) < self.min_connections {
175 guards.push(
176 self.pool
177 .acquire()
178 .await
179 .map_err(|e| RdbmsError::Connection(e.to_string()))?,
180 );
181 }
182 Ok(())
184 }
185}
186
187#[async_trait]
188impl SqlExecutor for SqlxClient {
189 async fn execute(&self, sql: &str) -> Result<u64, RdbmsError> {
190 self.timed(sql, async {
191 let affected = match &self.pool {
192 Pool::Pg(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
193 Pool::My(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
194 Pool::Sq(p) => sqlx::query(sql).execute(p).await.map(|r| r.rows_affected()),
195 }
196 .map_err(db_err)?;
197 Ok(affected)
198 })
199 .await
200 }
201
202 async fn query(&self, sql: &str) -> Result<Vec<Row>, RdbmsError> {
203 self.timed(sql, async {
204 let rows = match &self.pool {
205 Pool::Pg(p) => sqlx::query(sql)
206 .fetch_all(p)
207 .await
208 .map_err(db_err)
209 .and_then(pg_rows_to_result),
210 Pool::My(p) => sqlx::query(sql)
211 .fetch_all(p)
212 .await
213 .map_err(db_err)
214 .and_then(mysql_rows_to_result),
215 Pool::Sq(p) => sqlx::query(sql)
216 .fetch_all(p)
217 .await
218 .map_err(db_err)
219 .and_then(sqlite_rows_to_result),
220 }?;
221 Ok(rows)
222 })
223 .await
224 }
225
226 async fn execute_with(
227 &self,
228 sql: &str,
229 params: &[serde_json::Value],
230 ) -> Result<u64, RdbmsError> {
231 self.timed(sql, async {
232 let affected = match &self.pool {
233 Pool::Pg(p) => {
234 let mut q = sqlx::query(sql);
235 for param in params {
236 q = match param {
237 serde_json::Value::String(s) => q.bind(s.as_str()),
238 serde_json::Value::Number(n) => {
239 if let Some(i) = n.as_i64() {
240 q.bind(i)
241 } else if let Some(f) = n.as_f64() {
242 q.bind(f)
243 } else {
244 q.bind(n.to_string())
245 }
246 }
247 serde_json::Value::Bool(b) => q.bind(*b),
248 serde_json::Value::Null => q.bind(None::<String>),
249 _ => q.bind(param.to_string()),
250 };
251 }
252 q.execute(p).await.map(|r| r.rows_affected())
253 }
254 Pool::My(p) => {
255 let mut q = sqlx::query(sql);
256 for param in params {
257 q = match param {
258 serde_json::Value::String(s) => q.bind(s.as_str()),
259 serde_json::Value::Number(n) => {
260 if let Some(i) = n.as_i64() {
261 q.bind(i)
262 } else if let Some(f) = n.as_f64() {
263 q.bind(f)
264 } else {
265 q.bind(n.to_string())
266 }
267 }
268 serde_json::Value::Bool(b) => q.bind(*b),
269 serde_json::Value::Null => q.bind(None::<String>),
270 _ => q.bind(param.to_string()),
271 };
272 }
273 q.execute(p).await.map(|r| r.rows_affected())
274 }
275 Pool::Sq(p) => {
276 let mut q = sqlx::query(sql);
277 for param in params {
278 q = match param {
279 serde_json::Value::String(s) => q.bind(s.as_str()),
280 serde_json::Value::Number(n) => {
281 if let Some(i) = n.as_i64() {
282 q.bind(i)
283 } else if let Some(f) = n.as_f64() {
284 q.bind(f)
285 } else {
286 q.bind(n.to_string())
287 }
288 }
289 serde_json::Value::Bool(b) => q.bind(*b),
290 serde_json::Value::Null => q.bind(None::<String>),
291 _ => q.bind(param.to_string()),
292 };
293 }
294 q.execute(p).await.map(|r| r.rows_affected())
295 }
296 }
297 .map_err(db_err)?;
298 Ok(affected)
299 })
300 .await
301 }
302
303 async fn query_with(
304 &self,
305 sql: &str,
306 params: &[serde_json::Value],
307 ) -> Result<Vec<Row>, RdbmsError> {
308 self.timed(sql, async {
309 let rows = match &self.pool {
310 Pool::Pg(p) => {
311 let mut q = sqlx::query(sql);
312 for param in params {
313 q = match param {
314 serde_json::Value::String(s) => q.bind(s.as_str()),
315 serde_json::Value::Number(n) => {
316 if let Some(i) = n.as_i64() {
317 q.bind(i)
318 } else if let Some(f) = n.as_f64() {
319 q.bind(f)
320 } else {
321 q.bind(n.to_string())
322 }
323 }
324 serde_json::Value::Bool(b) => q.bind(*b),
325 serde_json::Value::Null => q.bind(None::<String>),
326 _ => q.bind(param.to_string()),
327 };
328 }
329 q.fetch_all(p)
330 .await
331 .map_err(db_err)
332 .and_then(pg_rows_to_result)
333 }
334 Pool::My(p) => {
335 let mut q = sqlx::query(sql);
336 for param in params {
337 q = match param {
338 serde_json::Value::String(s) => q.bind(s.as_str()),
339 serde_json::Value::Number(n) => {
340 if let Some(i) = n.as_i64() {
341 q.bind(i)
342 } else if let Some(f) = n.as_f64() {
343 q.bind(f)
344 } else {
345 q.bind(n.to_string())
346 }
347 }
348 serde_json::Value::Bool(b) => q.bind(*b),
349 serde_json::Value::Null => q.bind(None::<String>),
350 _ => q.bind(param.to_string()),
351 };
352 }
353 q.fetch_all(p)
354 .await
355 .map_err(db_err)
356 .and_then(mysql_rows_to_result)
357 }
358 Pool::Sq(p) => {
359 let mut q = sqlx::query(sql);
360 for param in params {
361 q = match param {
362 serde_json::Value::String(s) => q.bind(s.as_str()),
363 serde_json::Value::Number(n) => {
364 if let Some(i) = n.as_i64() {
365 q.bind(i)
366 } else if let Some(f) = n.as_f64() {
367 q.bind(f)
368 } else {
369 q.bind(n.to_string())
370 }
371 }
372 serde_json::Value::Bool(b) => q.bind(*b),
373 serde_json::Value::Null => q.bind(None::<String>),
374 _ => q.bind(param.to_string()),
375 };
376 }
377 q.fetch_all(p)
378 .await
379 .map_err(db_err)
380 .and_then(sqlite_rows_to_result)
381 }
382 }?;
383 Ok(rows)
384 })
385 .await
386 }
387
388 async fn execute_then_query(
394 &self,
395 first: &str,
396 first_params: &[serde_json::Value],
397 second: &str,
398 ) -> Result<Vec<Row>, RdbmsError> {
399 let tx = self.transaction().await?;
400 tx.execute_with(first, first_params).await?;
401 let rows = tx.query(second).await?;
402 tx.commit().await?;
403 Ok(rows)
404 }
405
406 fn dialect(&self) -> Dialect {
407 self.pool.dialect()
409 }
410}
411
412#[async_trait]
413impl RdbmsClient for SqlxClient {
414 async fn transaction(&self) -> Result<Transaction, RdbmsError> {
415 let dialect = self.pool.dialect();
416 let query_timeout = self.query_timeout;
419 let inner: Box<dyn TransactionInner> = match &self.pool {
420 Pool::Pg(p) => Box::new(PgTx {
421 inner: Some(p.begin().await.map_err(db_err)?),
422 dialect,
423 query_timeout,
424 }),
425 Pool::My(p) => Box::new(MyTx {
426 inner: Some(p.begin().await.map_err(db_err)?),
427 dialect,
428 query_timeout,
429 }),
430 Pool::Sq(p) => Box::new(SqTx {
431 inner: Some(p.begin().await.map_err(db_err)?),
432 dialect,
433 query_timeout,
434 }),
435 };
436 Ok(Transaction::with_inner(inner))
437 }
438}