1use std::future::Future;
10
11use super::backend::Backend;
12use super::connection::{DbConfig, DbTls};
13use super::migration;
14use super::{DbError, Model, ModelRow, MigrationStatus, Value};
15
16#[cfg(feature = "model-postgres")]
19#[doc(hidden)]
20pub fn pg_options(config: &DbConfig, tls: &DbTls) -> sqlx::postgres::PgConnectOptions {
21 use super::connection::DbSslMode;
22 use sqlx::postgres::{PgConnectOptions, PgSslMode};
23 let mut options = PgConnectOptions::new_without_pgpass()
24 .host(&config.host)
25 .port(config.port)
26 .username(&config.user)
27 .password(&config.password)
28 .database(&config.database)
29 .ssl_mode(match tls.mode {
30 DbSslMode::Disable => PgSslMode::Disable,
31 DbSslMode::Prefer => PgSslMode::Prefer,
32 DbSslMode::Require => PgSslMode::Require,
33 DbSslMode::VerifyCa => PgSslMode::VerifyCa,
34 DbSslMode::VerifyFull => PgSslMode::VerifyFull,
35 });
36 if let Some(ca) = &tls.root_cert {
37 options = options.ssl_root_cert(ca);
38 }
39 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
40 options = options.ssl_client_cert(cert).ssl_client_key(key);
41 }
42 options
43}
44
45#[cfg(feature = "model-mysql")]
46#[doc(hidden)]
47pub fn mysql_options(config: &DbConfig, tls: &DbTls) -> sqlx::mysql::MySqlConnectOptions {
48 use super::connection::DbSslMode;
49 use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode};
50 let mut options = MySqlConnectOptions::new()
51 .host(&config.host)
52 .port(config.port)
53 .username(&config.user)
54 .password(&config.password)
55 .database(&config.database)
56 .ssl_mode(match tls.mode {
57 DbSslMode::Disable => MySqlSslMode::Disabled,
58 DbSslMode::Prefer => MySqlSslMode::Preferred,
59 DbSslMode::Require => MySqlSslMode::Required,
60 DbSslMode::VerifyCa => MySqlSslMode::VerifyCa,
61 DbSslMode::VerifyFull => MySqlSslMode::VerifyIdentity,
62 });
63 if let Some(ca) = &tls.root_cert {
64 options = options.ssl_ca(ca);
65 }
66 if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
67 options = options.ssl_client_cert(cert).ssl_client_key(key);
68 }
69 options
70}
71
72#[derive(Clone, Debug)]
80pub enum DbPool {
81 #[cfg(feature = "model-sqlite")]
82 Sqlite(sqlx::Pool<sqlx::Sqlite>),
83 #[cfg(feature = "model-postgres")]
84 Postgres(sqlx::Pool<sqlx::Postgres>),
85 #[cfg(feature = "model-mysql")]
86 MySql(sqlx::Pool<sqlx::MySql>),
87}
88
89impl DbPool {
90 pub async fn new(config: DbConfig) -> Result<Self, DbError> {
94 DbPool::new_with_tls(config, DbTls::default()).await
95 }
96
97 pub async fn new_with_tls(config: DbConfig, tls: DbTls) -> Result<Self, DbError> {
101 let _ = &tls;
102 #[cfg(any(feature = "model-postgres", feature = "model-mysql"))]
108 let _ = crate::crypto_provider().install_default();
109 match config.backend {
110 #[cfg(feature = "model-sqlite")]
111 Backend::Sqlite => {
112 let pool = sqlx::pool::PoolOptions::<sqlx::Sqlite>::new()
113 .max_connections(config.pool_size)
114 .connect(&config.to_url())
115 .await
116 .map_err(|e| DbError::new(e.to_string()))?;
117 Ok(DbPool::Sqlite(pool))
118 }
119 #[cfg(feature = "model-postgres")]
120 Backend::Postgres => {
121 let pool = sqlx::pool::PoolOptions::<sqlx::Postgres>::new()
122 .max_connections(config.pool_size)
123 .connect_with(pg_options(&config, &tls))
124 .await
125 .map_err(|e| DbError::new(e.to_string()))?;
126 Ok(DbPool::Postgres(pool))
127 }
128 #[cfg(feature = "model-mysql")]
129 Backend::MySql => {
130 let pool = sqlx::pool::PoolOptions::<sqlx::MySql>::new()
131 .max_connections(config.pool_size)
132 .connect_with(mysql_options(&config, &tls))
133 .await
134 .map_err(|e| DbError::new(e.to_string()))?;
135 Ok(DbPool::MySql(pool))
136 }
137 }
138 }
139
140 pub async fn from_env() -> Result<Self, DbError> {
142 DbPool::new_with_tls(DbConfig::from_env()?, DbTls::from_env()?).await
143 }
144
145 #[cfg(feature = "model-sqlite")]
166 pub async fn memory() -> Result<Self, DbError> {
167 let pool = sqlx::pool::PoolOptions::<sqlx::Sqlite>::new()
168 .max_connections(1)
169 .connect("sqlite::memory:")
170 .await
171 .map_err(|e| DbError::new(e.to_string()))?;
172 Ok(DbPool::Sqlite(pool))
173 }
174
175 pub fn backend(&self) -> Backend {
177 match self {
178 #[cfg(feature = "model-sqlite")]
179 DbPool::Sqlite(_) => Backend::Sqlite,
180 #[cfg(feature = "model-postgres")]
181 DbPool::Postgres(_) => Backend::Postgres,
182 #[cfg(feature = "model-mysql")]
183 DbPool::MySql(_) => Backend::MySql,
184 }
185 }
186
187 pub async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, DbError> {
193 let span = db_span(self.backend(), sql);
194 let result = match self {
195 #[cfg(feature = "model-sqlite")]
196 DbPool::Sqlite(pool) => sqlite_pool_execute(pool, sql, params).await,
197 #[cfg(feature = "model-postgres")]
198 DbPool::Postgres(pool) => pg_pool_execute(pool, sql, params).await,
199 #[cfg(feature = "model-mysql")]
200 DbPool::MySql(pool) => mysql_pool_execute(pool, sql, params).await,
201 };
202 finish_db_span(span, &result);
203 result
204 }
205
206 pub async fn query_rows(&self, sql: &str, params: &[Value]) -> Result<Vec<ModelRow>, DbError> {
208 let span = db_span(self.backend(), sql);
209 let result = match self {
210 #[cfg(feature = "model-sqlite")]
211 DbPool::Sqlite(pool) => sqlite_pool_query_rows(pool, sql, params).await,
212 #[cfg(feature = "model-postgres")]
213 DbPool::Postgres(pool) => pg_pool_query_rows(pool, sql, params).await,
214 #[cfg(feature = "model-mysql")]
215 DbPool::MySql(pool) => mysql_pool_query_rows(pool, sql, params).await,
216 };
217 finish_db_span(span, &result);
218 result
219 }
220
221 pub async fn query<T: Model>(&self, sql: &str, params: &[Value]) -> Result<Vec<T>, DbError> {
223 let rows = self.query_rows(sql, params).await?;
224 rows.iter().map(|r| T::from_row(r)).collect()
225 }
226
227 pub async fn query_raw(&self, sql: &str, params: &[Value]) -> Result<Vec<ModelRow>, DbError> {
229 self.query_rows(sql, params).await
230 }
231
232 #[doc(hidden)]
236 pub async fn insert_returning_id(
237 &self,
238 insert_sql: &str,
239 params: &[Value],
240 _pk_col: &str,
241 ) -> Result<i64, DbError> {
242 let span = db_span(self.backend(), insert_sql);
243 let result = match self {
244 #[cfg(feature = "model-sqlite")]
245 DbPool::Sqlite(pool) => sqlite_last_insert_id(pool, insert_sql, params).await,
246 #[cfg(feature = "model-postgres")]
247 DbPool::Postgres(pool) => pg_insert_returning(pool, insert_sql, params, _pk_col).await,
248 #[cfg(feature = "model-mysql")]
249 DbPool::MySql(pool) => mysql_last_insert_id(pool, insert_sql, params).await,
250 };
251 finish_db_span(span, &result);
252 result
253 }
254
255 pub async fn begin(&self) -> Result<DbTransaction, DbError> {
260 match self {
261 #[cfg(feature = "model-sqlite")]
262 DbPool::Sqlite(pool) => {
263 let tx = pool.begin().await.map_err(|e| DbError::new(e.to_string()))?;
264 Ok(DbTransaction::Sqlite(tx))
265 }
266 #[cfg(feature = "model-postgres")]
267 DbPool::Postgres(pool) => {
268 let tx = pool.begin().await.map_err(|e| DbError::new(e.to_string()))?;
269 Ok(DbTransaction::Postgres(tx))
270 }
271 #[cfg(feature = "model-mysql")]
272 DbPool::MySql(pool) => {
273 let tx = pool.begin().await.map_err(|e| DbError::new(e.to_string()))?;
274 Ok(DbTransaction::MySql(tx))
275 }
276 }
277 }
278
279 pub async fn transaction<F, T, Fut>(&self, f: F) -> Result<T, DbError>
302 where
303 F: FnOnce(DbTransaction) -> Fut + Send,
304 Fut: Future<Output = Result<T, DbError>> + Send,
305 T: Send,
306 {
307 let tx = self.begin().await?;
308 match f(tx).await {
309 Ok(v) => Ok(v),
310 Err(e) => Err(e),
311 }
312 }
313
314 pub async fn migrate(&self, dir: &str) -> Result<(), DbError> {
321 migration::run_migrations(self, dir).await
322 }
323
324 pub async fn migration_status(&self, dir: &str) -> Result<Vec<MigrationStatus>, DbError> {
326 migration::migration_status(self, dir).await
327 }
328
329 pub async fn rollback_last(&self, dir: &str) -> Result<Option<String>, DbError> {
336 migration::rollback_last(self, dir).await
337 }
338
339 pub async fn rollback(&self, dir: &str, n: usize) -> Result<Vec<String>, DbError> {
345 migration::rollback(self, dir, n).await
346 }
347
348 pub async fn seed(&self, dir: &str) -> Result<Vec<String>, DbError> {
356 migration::run_seeds(self, dir).await
357 }
358
359 pub async fn connect(url: &str) -> Result<Self, DbError> {
365 let url = url.trim();
366 let scheme = url.split(':').next().unwrap_or("").to_ascii_lowercase();
367 let backend = match scheme.as_str() {
368 "sqlite" => "sqlite",
369 "postgres" | "postgresql" => "postgres",
370 "mysql" | "mariadb" => "mysql",
371 _ => {
372 return Err(DbError::new(format!(
373 "database URL {:?}: expected sqlite:<path>, postgres://… or mysql://…",
374 redact_url(url)
375 )))
376 }
377 };
378 let backend = Backend::parse(backend)
379 .map_err(|e| DbError::new(format!("database URL {:?}: {}", redact_url(url), e.0)))?;
380 #[cfg(any(feature = "model-postgres", feature = "model-mysql"))]
381 let _ = crate::crypto_provider().install_default();
382 let bad_url = |e: sqlx::Error| DbError::new(format!("database URL {:?}: {e}", redact_url(url)));
383 match backend {
384 #[cfg(feature = "model-sqlite")]
385 Backend::Sqlite => {
386 let options: sqlx::sqlite::SqliteConnectOptions = url.parse().map_err(bad_url)?;
387 let pool = sqlx::pool::PoolOptions::<sqlx::Sqlite>::new()
388 .max_connections(1)
389 .connect_with(options)
390 .await
391 .map_err(|e| DbError::new(e.to_string()))?;
392 Ok(DbPool::Sqlite(pool))
393 }
394 #[cfg(feature = "model-postgres")]
395 Backend::Postgres => {
396 let options: sqlx::postgres::PgConnectOptions = url.parse().map_err(bad_url)?;
397 let pool = sqlx::pool::PoolOptions::<sqlx::Postgres>::new()
398 .max_connections(4)
399 .connect_with(options)
400 .await
401 .map_err(|e| DbError::new(e.to_string()))?;
402 Ok(DbPool::Postgres(pool))
403 }
404 #[cfg(feature = "model-mysql")]
405 Backend::MySql => {
406 let options: sqlx::mysql::MySqlConnectOptions = url.parse().map_err(bad_url)?;
407 let pool = sqlx::pool::PoolOptions::<sqlx::MySql>::new()
408 .max_connections(4)
409 .connect_with(options)
410 .await
411 .map_err(|e| DbError::new(e.to_string()))?;
412 Ok(DbPool::MySql(pool))
413 }
414 }
415 }
416}
417
418#[doc(hidden)]
420pub fn redact_url(url: &str) -> String {
421 let Some((scheme, rest)) = url.split_once("://") else { return url.to_string() };
422 let authority_end = rest.find('/').unwrap_or(rest.len());
423 let (authority, tail) = rest.split_at(authority_end);
424 match authority.rsplit_once('@') {
425 Some((userinfo, host)) => match userinfo.split_once(':') {
426 Some((user, _)) => format!("{scheme}://{user}:***@{host}{tail}"),
427 None => url.to_string(),
428 },
429 None => url.to_string(),
430 }
431}
432
433pub enum DbTransaction {
440 #[cfg(feature = "model-sqlite")]
441 Sqlite(sqlx::Transaction<'static, sqlx::Sqlite>),
442 #[cfg(feature = "model-postgres")]
443 Postgres(sqlx::Transaction<'static, sqlx::Postgres>),
444 #[cfg(feature = "model-mysql")]
445 MySql(sqlx::Transaction<'static, sqlx::MySql>),
446}
447
448impl DbTransaction {
449 fn backend(&self) -> Backend {
450 match self {
451 #[cfg(feature = "model-sqlite")]
452 DbTransaction::Sqlite(_) => Backend::Sqlite,
453 #[cfg(feature = "model-postgres")]
454 DbTransaction::Postgres(_) => Backend::Postgres,
455 #[cfg(feature = "model-mysql")]
456 DbTransaction::MySql(_) => Backend::MySql,
457 }
458 }
459
460 pub async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, DbError> {
462 let span = db_span(self.backend(), sql);
463 let result = match self {
464 #[cfg(feature = "model-sqlite")]
465 DbTransaction::Sqlite(tx) => sqlite_tx_execute(tx, sql, params).await,
466 #[cfg(feature = "model-postgres")]
467 DbTransaction::Postgres(tx) => pg_tx_execute(tx, sql, params).await,
468 #[cfg(feature = "model-mysql")]
469 DbTransaction::MySql(tx) => mysql_tx_execute(tx, sql, params).await,
470 };
471 finish_db_span(span, &result);
472 result
473 }
474
475 pub async fn query_rows(&mut self, sql: &str, params: &[Value]) -> Result<Vec<ModelRow>, DbError> {
477 let span = db_span(self.backend(), sql);
478 let result = match self {
479 #[cfg(feature = "model-sqlite")]
480 DbTransaction::Sqlite(tx) => sqlite_tx_query_rows(tx, sql, params).await,
481 #[cfg(feature = "model-postgres")]
482 DbTransaction::Postgres(tx) => pg_tx_query_rows(tx, sql, params).await,
483 #[cfg(feature = "model-mysql")]
484 DbTransaction::MySql(tx) => mysql_tx_query_rows(tx, sql, params).await,
485 };
486 finish_db_span(span, &result);
487 result
488 }
489
490 pub async fn query<T: Model>(&mut self, sql: &str, params: &[Value]) -> Result<Vec<T>, DbError> {
492 let rows = self.query_rows(sql, params).await?;
493 rows.iter().map(|r| T::from_row(r)).collect()
494 }
495
496 pub async fn commit(self) -> Result<(), DbError> {
498 match self {
499 #[cfg(feature = "model-sqlite")]
500 DbTransaction::Sqlite(tx) => tx.commit().await.map_err(|e| DbError::new(e.to_string())),
501 #[cfg(feature = "model-postgres")]
502 DbTransaction::Postgres(tx) => tx.commit().await.map_err(|e| DbError::new(e.to_string())),
503 #[cfg(feature = "model-mysql")]
504 DbTransaction::MySql(tx) => tx.commit().await.map_err(|e| DbError::new(e.to_string())),
505 }
506 }
507
508 pub async fn rollback(self) -> Result<(), DbError> {
510 match self {
511 #[cfg(feature = "model-sqlite")]
512 DbTransaction::Sqlite(tx) => tx.rollback().await.map_err(|e| DbError::new(e.to_string())),
513 #[cfg(feature = "model-postgres")]
514 DbTransaction::Postgres(tx) => tx.rollback().await.map_err(|e| DbError::new(e.to_string())),
515 #[cfg(feature = "model-mysql")]
516 DbTransaction::MySql(tx) => tx.rollback().await.map_err(|e| DbError::new(e.to_string())),
517 }
518 }
519}
520
521#[cfg(feature = "model-sqlite")]
524async fn sqlite_pool_execute(
525 pool: &sqlx::Pool<sqlx::Sqlite>,
526 sql: &str,
527 params: &[Value],
528) -> Result<u64, DbError> {
529 let mut args = sqlx::sqlite::SqliteArguments::default();
530 bind_sqlite_args(&mut args, params)?;
531 let r = sqlx::query_with(sql, args)
532 .execute(pool)
533 .await
534 .map_err(|e| DbError::new(e.to_string()))?;
535 Ok(r.rows_affected())
536}
537
538#[cfg(feature = "model-sqlite")]
539async fn sqlite_pool_query_rows(
540 pool: &sqlx::Pool<sqlx::Sqlite>,
541 sql: &str,
542 params: &[Value],
543) -> Result<Vec<ModelRow>, DbError> {
544 let mut args = sqlx::sqlite::SqliteArguments::default();
545 bind_sqlite_args(&mut args, params)?;
546 let rows = sqlx::query_with(sql, args)
547 .fetch_all(pool)
548 .await
549 .map_err(|e| DbError::new(e.to_string()))?;
550 rows.into_iter().map(sqlite_row_to_model_row).collect()
551}
552
553#[cfg(feature = "model-sqlite")]
554async fn sqlite_tx_execute(
555 tx: &mut sqlx::Transaction<'static, sqlx::Sqlite>,
556 sql: &str,
557 params: &[Value],
558) -> Result<u64, DbError> {
559 let mut args = sqlx::sqlite::SqliteArguments::default();
560 bind_sqlite_args(&mut args, params)?;
561 let r = sqlx::query_with(sql, args)
562 .execute(&mut **tx)
563 .await
564 .map_err(|e| DbError::new(e.to_string()))?;
565 Ok(r.rows_affected())
566}
567
568#[cfg(feature = "model-sqlite")]
569async fn sqlite_tx_query_rows(
570 tx: &mut sqlx::Transaction<'static, sqlx::Sqlite>,
571 sql: &str,
572 params: &[Value],
573) -> Result<Vec<ModelRow>, DbError> {
574 let mut args = sqlx::sqlite::SqliteArguments::default();
575 bind_sqlite_args(&mut args, params)?;
576 let rows = sqlx::query_with(sql, args)
577 .fetch_all(&mut **tx)
578 .await
579 .map_err(|e| DbError::new(e.to_string()))?;
580 rows.into_iter().map(sqlite_row_to_model_row).collect()
581}
582
583#[cfg(feature = "model-sqlite")]
584fn bind_sqlite_args(
585 args: &mut sqlx::sqlite::SqliteArguments<'_>,
586 params: &[Value],
587) -> Result<(), DbError> {
588 use sqlx::Arguments;
589 for v in params {
590 match v {
591 Value::Null => args.add(None::<String>).map_err(|e| DbError::new(e.to_string()))?,
592 Value::Bool(b) => args.add(*b).map_err(|e| DbError::new(e.to_string()))?,
593 Value::Int(i) => args.add(*i).map_err(|e| DbError::new(e.to_string()))?,
594 Value::Float(f) => args.add(*f).map_err(|e| DbError::new(e.to_string()))?,
595 Value::Text(s) => args.add(s.clone()).map_err(|e| DbError::new(e.to_string()))?,
596 Value::Bytes(b) => args.add(b.clone()).map_err(|e| DbError::new(e.to_string()))?,
597 }
598 }
599 Ok(())
600}
601
602#[cfg(feature = "model-sqlite")]
603fn sqlite_row_to_model_row(
604 row: sqlx::sqlite::SqliteRow,
605) -> Result<ModelRow, DbError> {
606 use sqlx::{Column, Row, TypeInfo};
607 let mut cols: Vec<(String, Value)> = Vec::with_capacity(row.columns().len());
608 for col in row.columns() {
609 let name = col.name().to_string();
610 let type_name = col.type_info().name();
611 let value = if type_name.contains("INT") {
612 row.try_get::<Option<i64>, _>(col.ordinal())
613 .map(|v| v.map(Value::Int).unwrap_or(Value::Null))
614 .unwrap_or(Value::Null)
615 } else if type_name.contains("REAL")
616 || type_name.contains("FLOAT")
617 || type_name.contains("DOUBLE")
618 || type_name.contains("NUMERIC")
619 || type_name.contains("DECIMAL")
620 {
621 row.try_get::<Option<f64>, _>(col.ordinal())
622 .map(|v| v.map(Value::Float).unwrap_or(Value::Null))
623 .unwrap_or(Value::Null)
624 } else if type_name.contains("BOOL") {
625 row.try_get::<Option<bool>, _>(col.ordinal())
626 .map(|v| v.map(Value::Bool).unwrap_or(Value::Null))
627 .unwrap_or(Value::Null)
628 } else if type_name.contains("BLOB") {
629 row.try_get::<Option<Vec<u8>>, _>(col.ordinal())
630 .map(|v| v.map(Value::Bytes).unwrap_or(Value::Null))
631 .unwrap_or(Value::Null)
632 } else if type_name.is_empty() || type_name == "NULL" {
633 if let Ok(Some(v)) = row.try_get::<Option<i64>, _>(col.ordinal()) {
635 Value::Int(v)
636 } else if let Ok(Some(v)) = row.try_get::<Option<f64>, _>(col.ordinal()) {
637 Value::Float(v)
638 } else if let Ok(Some(v)) = row.try_get::<Option<String>, _>(col.ordinal()) {
639 Value::Text(v)
640 } else if let Ok(Some(v)) = row.try_get::<Option<Vec<u8>>, _>(col.ordinal()) {
641 Value::Bytes(v)
642 } else {
643 Value::Null
644 }
645 } else {
646 match row.try_get::<Option<String>, _>(col.ordinal()) {
649 Ok(v) => v.map(Value::Text).unwrap_or(Value::Null),
650 Err(_) => sqlite_stored_value(&row, col.ordinal()),
651 }
652 };
653 cols.push((name, value));
654 }
655 Ok(ModelRow::new(cols))
656}
657
658#[cfg(feature = "model-sqlite")]
660fn sqlite_stored_value(row: &sqlx::sqlite::SqliteRow, ordinal: usize) -> Value {
661 use sqlx::{Row, TypeInfo, ValueRef};
662 let Ok(raw) = row.try_get_raw(ordinal) else { return Value::Null };
663 if raw.is_null() {
664 return Value::Null;
665 }
666 let class = raw.type_info().name().to_string();
667 match class.as_str() {
668 "INTEGER" => row.try_get_unchecked::<i64, _>(ordinal).map(Value::Int).unwrap_or(Value::Null),
669 "REAL" => row.try_get_unchecked::<f64, _>(ordinal).map(Value::Float).unwrap_or(Value::Null),
670 "BLOB" => row.try_get_unchecked::<Vec<u8>, _>(ordinal).map(Value::Bytes).unwrap_or(Value::Null),
671 _ => row.try_get_unchecked::<String, _>(ordinal).map(Value::Text).unwrap_or(Value::Null),
672 }
673}
674
675#[cfg(feature = "model-sqlite")]
678async fn sqlite_last_insert_id(
679 pool: &sqlx::Pool<sqlx::Sqlite>,
680 sql: &str,
681 params: &[Value],
682) -> Result<i64, DbError> {
683 let mut args = sqlx::sqlite::SqliteArguments::default();
684 bind_sqlite_args(&mut args, params)?;
685 let r = sqlx::query_with(sql, args)
686 .execute(pool)
687 .await
688 .map_err(|e| DbError::new(e.to_string()))?;
689 Ok(r.last_insert_rowid())
690}
691
692#[cfg(feature = "model-postgres")]
695async fn pg_pool_execute(
696 pool: &sqlx::Pool<sqlx::Postgres>,
697 sql: &str,
698 params: &[Value],
699) -> Result<u64, DbError> {
700 let mut args = sqlx::postgres::PgArguments::default();
701 bind_pg_args(&mut args, params)?;
702 let r = sqlx::query_with(sql, args)
703 .execute(pool)
704 .await
705 .map_err(|e| DbError::new(e.to_string()))?;
706 Ok(r.rows_affected())
707}
708
709#[cfg(feature = "model-postgres")]
710async fn pg_pool_query_rows(
711 pool: &sqlx::Pool<sqlx::Postgres>,
712 sql: &str,
713 params: &[Value],
714) -> Result<Vec<ModelRow>, DbError> {
715 let mut args = sqlx::postgres::PgArguments::default();
716 bind_pg_args(&mut args, params)?;
717 let rows = sqlx::query_with(sql, args)
718 .fetch_all(pool)
719 .await
720 .map_err(|e| DbError::new(e.to_string()))?;
721 rows.into_iter().map(pg_row_to_model_row).collect()
722}
723
724#[cfg(feature = "model-postgres")]
725async fn pg_tx_execute(
726 tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
727 sql: &str,
728 params: &[Value],
729) -> Result<u64, DbError> {
730 let mut args = sqlx::postgres::PgArguments::default();
731 bind_pg_args(&mut args, params)?;
732 let r = sqlx::query_with(sql, args)
733 .execute(&mut **tx)
734 .await
735 .map_err(|e| DbError::new(e.to_string()))?;
736 Ok(r.rows_affected())
737}
738
739#[cfg(feature = "model-postgres")]
740async fn pg_tx_query_rows(
741 tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
742 sql: &str,
743 params: &[Value],
744) -> Result<Vec<ModelRow>, DbError> {
745 let mut args = sqlx::postgres::PgArguments::default();
746 bind_pg_args(&mut args, params)?;
747 let rows = sqlx::query_with(sql, args)
748 .fetch_all(&mut **tx)
749 .await
750 .map_err(|e| DbError::new(e.to_string()))?;
751 rows.into_iter().map(pg_row_to_model_row).collect()
752}
753
754#[cfg(feature = "model-postgres")]
755fn bind_pg_args(
756 args: &mut sqlx::postgres::PgArguments,
757 params: &[Value],
758) -> Result<(), DbError> {
759 use sqlx::Arguments;
760 for v in params {
761 match v {
762 Value::Null => args.add(None::<String>).map_err(|e| DbError::new(e.to_string()))?,
763 Value::Bool(b) => args.add(*b).map_err(|e| DbError::new(e.to_string()))?,
764 Value::Int(i) => args.add(*i).map_err(|e| DbError::new(e.to_string()))?,
765 Value::Float(f) => args.add(*f).map_err(|e| DbError::new(e.to_string()))?,
766 Value::Text(s) => args.add(s.clone()).map_err(|e| DbError::new(e.to_string()))?,
767 Value::Bytes(b) => args.add(b.clone()).map_err(|e| DbError::new(e.to_string()))?,
768 }
769 }
770 Ok(())
771}
772
773#[cfg(feature = "model-postgres")]
774fn pg_row_to_model_row(row: sqlx::postgres::PgRow) -> Result<ModelRow, DbError> {
775 use sqlx::{Column, Row, TypeInfo};
776 let mut cols: Vec<(String, Value)> = Vec::with_capacity(row.columns().len());
777 for col in row.columns() {
778 let name = col.name().to_string();
779 let type_name = col.type_info().name();
780 let value = match type_name {
783 "BOOL" => row.try_get::<Option<bool>, _>(col.ordinal())
784 .map(|v| v.map(Value::Bool).unwrap_or(Value::Null))
785 .unwrap_or(Value::Null),
786 "INT2" => row.try_get::<Option<i16>, _>(col.ordinal())
787 .map(|v| v.map(|n| Value::Int(n as i64)).unwrap_or(Value::Null))
788 .unwrap_or(Value::Null),
789 "INT4" => row.try_get::<Option<i32>, _>(col.ordinal())
790 .map(|v| v.map(|n| Value::Int(n as i64)).unwrap_or(Value::Null))
791 .unwrap_or(Value::Null),
792 "INT8" => row.try_get::<Option<i64>, _>(col.ordinal())
793 .map(|v| v.map(Value::Int).unwrap_or(Value::Null))
794 .unwrap_or(Value::Null),
795 "FLOAT4" => row.try_get::<Option<f32>, _>(col.ordinal())
796 .map(|v| v.map(|f| Value::Float(f as f64)).unwrap_or(Value::Null))
797 .unwrap_or(Value::Null),
798 "FLOAT8" | "NUMERIC" => row.try_get::<Option<f64>, _>(col.ordinal())
799 .map(|v| v.map(Value::Float).unwrap_or(Value::Null))
800 .unwrap_or(Value::Null),
801 "TIMESTAMPTZ" | "TIMESTAMP" | "DATE" | "UUID" => {
802 use sqlx::ValueRef;
803 match row.try_get_raw(col.ordinal()) {
804 Ok(raw) if !raw.is_null() => {
805 let binary = matches!(raw.format(), sqlx::postgres::PgValueFormat::Binary);
806 raw.as_bytes().ok().and_then(|bytes| super::decode::pg_value(type_name, binary, bytes)).unwrap_or(Value::Null)
807 }
808 _ => Value::Null,
809 }
810 }
811 "BYTEA" => row.try_get::<Option<Vec<u8>>, _>(col.ordinal())
812 .map(|v| v.map(Value::Bytes).unwrap_or(Value::Null))
813 .unwrap_or(Value::Null),
814 _ => row.try_get::<Option<String>, _>(col.ordinal())
815 .map(|v| v.map(Value::Text).unwrap_or(Value::Null))
816 .unwrap_or(Value::Null),
817 };
818 cols.push((name, value));
819 }
820 Ok(ModelRow::new(cols))
821}
822
823#[cfg(feature = "model-postgres")]
825async fn pg_insert_returning(
826 pool: &sqlx::Pool<sqlx::Postgres>,
827 sql: &str,
828 params: &[Value],
829 pk_col: &str,
830) -> Result<i64, DbError> {
831 let mut args = sqlx::postgres::PgArguments::default();
832 bind_pg_args(&mut args, params)?;
833 let returning_sql = format!("{} RETURNING {}", sql, pk_col);
834 let row = sqlx::query_with(&returning_sql, args)
835 .fetch_one(pool)
836 .await
837 .map_err(|e| DbError::new(e.to_string()))?;
838 match pg_row_to_model_row(row)?.columns.into_iter().next() {
841 Some((_, Value::Int(id))) => Ok(id),
842 other => Err(DbError::new(format!("INSERT … RETURNING {pk_col}: expected an integer key, got {other:?}"))),
843 }
844}
845
846#[cfg(feature = "model-mysql")]
849async fn mysql_pool_execute(
850 pool: &sqlx::Pool<sqlx::MySql>,
851 sql: &str,
852 params: &[Value],
853) -> Result<u64, DbError> {
854 let mut args = sqlx::mysql::MySqlArguments::default();
855 bind_mysql_args(&mut args, params)?;
856 let r = sqlx::query_with(sql, args)
857 .execute(pool)
858 .await
859 .map_err(|e| DbError::new(e.to_string()))?;
860 Ok(r.rows_affected())
861}
862
863#[cfg(feature = "model-mysql")]
864async fn mysql_pool_query_rows(
865 pool: &sqlx::Pool<sqlx::MySql>,
866 sql: &str,
867 params: &[Value],
868) -> Result<Vec<ModelRow>, DbError> {
869 let mut args = sqlx::mysql::MySqlArguments::default();
870 bind_mysql_args(&mut args, params)?;
871 let rows = sqlx::query_with(sql, args)
872 .fetch_all(pool)
873 .await
874 .map_err(|e| DbError::new(e.to_string()))?;
875 rows.into_iter().map(mysql_row_to_model_row).collect()
876}
877
878#[cfg(feature = "model-mysql")]
879async fn mysql_tx_execute(
880 tx: &mut sqlx::Transaction<'static, sqlx::MySql>,
881 sql: &str,
882 params: &[Value],
883) -> Result<u64, DbError> {
884 let mut args = sqlx::mysql::MySqlArguments::default();
885 bind_mysql_args(&mut args, params)?;
886 let r = sqlx::query_with(sql, args)
887 .execute(&mut **tx)
888 .await
889 .map_err(|e| DbError::new(e.to_string()))?;
890 Ok(r.rows_affected())
891}
892
893#[cfg(feature = "model-mysql")]
894async fn mysql_tx_query_rows(
895 tx: &mut sqlx::Transaction<'static, sqlx::MySql>,
896 sql: &str,
897 params: &[Value],
898) -> Result<Vec<ModelRow>, DbError> {
899 let mut args = sqlx::mysql::MySqlArguments::default();
900 bind_mysql_args(&mut args, params)?;
901 let rows = sqlx::query_with(sql, args)
902 .fetch_all(&mut **tx)
903 .await
904 .map_err(|e| DbError::new(e.to_string()))?;
905 rows.into_iter().map(mysql_row_to_model_row).collect()
906}
907
908#[cfg(feature = "model-mysql")]
909fn bind_mysql_args(
910 args: &mut sqlx::mysql::MySqlArguments,
911 params: &[Value],
912) -> Result<(), DbError> {
913 use sqlx::Arguments;
914 for v in params {
915 match v {
916 Value::Null => args.add(None::<String>).map_err(|e| DbError::new(e.to_string()))?,
917 Value::Bool(b) => args.add(*b).map_err(|e| DbError::new(e.to_string()))?,
918 Value::Int(i) => args.add(*i).map_err(|e| DbError::new(e.to_string()))?,
919 Value::Float(f) => args.add(*f).map_err(|e| DbError::new(e.to_string()))?,
920 Value::Text(s) => args.add(s.clone()).map_err(|e| DbError::new(e.to_string()))?,
921 Value::Bytes(b) => args.add(b.clone()).map_err(|e| DbError::new(e.to_string()))?,
922 }
923 }
924 Ok(())
925}
926
927#[cfg(feature = "model-mysql")]
928fn mysql_row_to_model_row(row: sqlx::mysql::MySqlRow) -> Result<ModelRow, DbError> {
929 use sqlx::{Column, Row, TypeInfo};
930 let mut cols: Vec<(String, Value)> = Vec::with_capacity(row.columns().len());
931 for col in row.columns() {
932 let name = col.name().to_string();
933 let type_name = col.type_info().name().to_uppercase();
934 let value = if type_name.contains("INT") || type_name.contains("SERIAL") {
935 match row.try_get::<Option<i64>, _>(col.ordinal()) {
936 Ok(v) => v.map(Value::Int).unwrap_or(Value::Null),
937 Err(_) => row
939 .try_get::<Option<u64>, _>(col.ordinal())
940 .map(|v| v.map(|n| Value::Int(n as i64)).unwrap_or(Value::Null))
941 .unwrap_or(Value::Null),
942 }
943 } else if matches!(type_name.as_str(), "DATETIME" | "TIMESTAMP" | "DATE") {
944 row.try_get_unchecked::<Option<Vec<u8>>, _>(col.ordinal())
947 .ok()
948 .flatten()
949 .and_then(|bytes| super::decode::mysql_temporal(&type_name, &bytes))
950 .unwrap_or(Value::Null)
951 } else if type_name.contains("FLOAT")
952 || type_name.contains("DOUBLE")
953 || type_name.contains("DECIMAL")
954 || type_name.contains("NUMERIC")
955 {
956 row.try_get::<Option<f64>, _>(col.ordinal())
957 .map(|v| v.map(Value::Float).unwrap_or(Value::Null))
958 .unwrap_or(Value::Null)
959 } else if type_name.contains("BOOL") {
960 row.try_get::<Option<bool>, _>(col.ordinal())
961 .map(|v| v.map(Value::Bool).unwrap_or(Value::Null))
962 .unwrap_or(Value::Null)
963 } else if type_name.contains("BLOB") || type_name.contains("BINARY") {
964 row.try_get::<Option<Vec<u8>>, _>(col.ordinal())
965 .map(|v| v.map(Value::Bytes).unwrap_or(Value::Null))
966 .unwrap_or(Value::Null)
967 } else {
968 row.try_get::<Option<String>, _>(col.ordinal())
969 .map(|v| v.map(Value::Text).unwrap_or(Value::Null))
970 .unwrap_or(Value::Null)
971 };
972 cols.push((name, value));
973 }
974 Ok(ModelRow::new(cols))
975}
976
977#[cfg(feature = "model-mysql")]
980async fn mysql_last_insert_id(
981 pool: &sqlx::Pool<sqlx::MySql>,
982 sql: &str,
983 params: &[Value],
984) -> Result<i64, DbError> {
985 let mut args = sqlx::mysql::MySqlArguments::default();
986 bind_mysql_args(&mut args, params)?;
987 let r = sqlx::query_with(sql, args)
988 .execute(pool)
989 .await
990 .map_err(|e| DbError::new(e.to_string()))?;
991 Ok(r.last_insert_id() as i64)
992}
993
994fn db_span(backend: Backend, sql: &str) -> Option<crate::otel::DetachedSpan> {
1000 let operation = sql_operation(sql);
1001 let mut span = crate::otel::detached_span(operation, crate::otel::SpanKind::Client)?;
1002 let system = match backend {
1003 #[cfg(feature = "model-sqlite")]
1004 Backend::Sqlite => "sqlite",
1005 #[cfg(feature = "model-postgres")]
1006 Backend::Postgres => "postgresql",
1007 #[cfg(feature = "model-mysql")]
1008 Backend::MySql => "mysql",
1009 };
1010 span.set_attribute("db.system.name", system);
1011 span.set_attribute("db.operation.name", operation);
1012 Some(span)
1013}
1014
1015#[cfg(all(test, feature = "model-sqlite"))]
1016#[doc(hidden)]
1017pub fn db_span_for_test(sql: &str) -> Option<crate::otel::DetachedSpan> {
1018 db_span(Backend::Sqlite, sql)
1019}
1020
1021fn finish_db_span<T>(span: Option<crate::otel::DetachedSpan>, result: &Result<T, DbError>) {
1022 if let (Some(mut span), Err(e)) = (span, result) {
1023 span.record_error(&e.to_string());
1024 }
1025}
1026
1027#[doc(hidden)]
1029pub fn sql_operation(sql: &str) -> &'static str {
1030 const KNOWN: [&str; 14] = [
1031 "SELECT", "INSERT", "UPDATE", "DELETE", "WITH", "CREATE", "ALTER", "DROP", "BEGIN", "COMMIT", "ROLLBACK", "PRAGMA", "REPLACE", "TRUNCATE",
1032 ];
1033 let word = sql.trim_start().split(|c: char| !c.is_ascii_alphabetic()).next().unwrap_or("");
1034 KNOWN.iter().find(|k| k.eq_ignore_ascii_case(word)).copied().unwrap_or("query")
1035}