use super::{Db, DbError, FetchedThen, RawRows, Tx};
use crate::error::DatabaseError;
use crate::row_json::{row_to_json, row_to_values};
use postrust_sql::SqlParam;
use sqlx::postgres::{PgArguments, PgPool, PgRow};
use sqlx::query::Query;
use sqlx::{Postgres, Row, ValueRef};
pub struct SqlxTx {
tx: sqlx::Transaction<'static, Postgres>,
queued: Vec<(String, Vec<SqlParam>)>,
}
impl Db for PgPool {
type Tx<'a> = SqlxTx;
async fn begin(&self) -> Result<SqlxTx, DbError> {
Ok(SqlxTx {
tx: PgPool::begin(self).await?,
queued: Vec::new(),
})
}
}
impl SqlxTx {
async fn flush(&mut self) -> Result<(), DbError> {
for (sql, params) in std::mem::take(&mut self.queued) {
bind(sqlx::query(&sql), ¶ms)
.execute(&mut *self.tx)
.await
.map_err(|e| DbError::from(e).queued())?;
}
Ok(())
}
}
impl Tx for SqlxTx {
async fn execute(&mut self, sql: &str, params: &[SqlParam]) -> Result<u64, DbError> {
self.flush().await?;
Ok(bind(sqlx::query(sql), params)
.execute(&mut *self.tx)
.await?
.rows_affected())
}
async fn fetch_json(
&mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<serde_json::Value>, DbError> {
self.flush().await?;
Ok(fetch_all(&mut self.tx, sql, params)
.await?
.into_iter()
.map(|row| row_to_json(&row))
.collect())
}
async fn fetch_values(
&mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<Vec<serde_json::Value>>, DbError> {
self.flush().await?;
Ok(fetch_all(&mut self.tx, sql, params)
.await?
.into_iter()
.map(|row| row_to_values(&row))
.collect())
}
async fn fetch_raw(
&mut self,
sql: &str,
params: &[SqlParam],
count_column: &str,
) -> Result<RawRows, DbError> {
self.flush().await?;
let rows = fetch_all(&mut self.tx, sql, params).await?;
let first = rows.first().and_then(|row| {
let value = row.try_get_raw(0).ok()?;
if value.is_null() {
return None;
}
Some(value.as_bytes().ok()?.to_vec())
});
let count = rows
.first()
.and_then(|row| row.try_get::<i64, _>(count_column).ok());
Ok(RawRows::new(rows.len(), first, count))
}
async fn commit(mut self) -> Result<(), DbError> {
self.flush().await?;
Ok(self.tx.commit().await?)
}
fn queue(&mut self, sql: &str, params: &[SqlParam]) {
self.queued.push((sql.to_string(), params.to_vec()));
}
async fn fetch_json_then(&mut self, sql: &str, params: &[SqlParam], then: &str) -> FetchedThen {
let rows = self.fetch_json(sql, params).await?;
let then = self.fetch_values(then, &[]).await?;
Ok((rows, then))
}
async fn fetch_json_and_commit(
mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<serde_json::Value>, DbError> {
let rows = self.fetch_json(sql, params).await?;
self.commit().await?;
Ok(rows)
}
}
async fn fetch_all(
tx: &mut sqlx::Transaction<'static, Postgres>,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<PgRow>, sqlx::Error> {
bind(sqlx::query(sql), params).fetch_all(&mut **tx).await
}
fn bind<'q>(
mut query: Query<'q, Postgres, PgArguments>,
params: &'q [SqlParam],
) -> Query<'q, Postgres, PgArguments> {
for param in params {
query = match param {
SqlParam::Null => query.bind(None::<String>),
SqlParam::Bool(b) => query.bind(b),
SqlParam::Int(n) => query.bind(n),
SqlParam::Float(f) => query.bind(f),
SqlParam::Text(s) => query.bind(s),
SqlParam::Bytes(b) => query.bind(b),
SqlParam::Json(j) => query.bind(j),
SqlParam::Uuid(u) => query.bind(u),
SqlParam::Timestamp(t) => query.bind(t),
SqlParam::Array(arr) => {
let strings: Vec<String> = arr
.iter()
.map(|p| match p {
SqlParam::Text(s) => s.clone(),
SqlParam::Int(n) => n.to_string(),
SqlParam::Bool(b) => b.to_string(),
other => format!("{:?}", other),
})
.collect();
query.bind(strings)
}
};
}
query
}
impl From<sqlx::Error> for DbError {
fn from(e: sqlx::Error) -> Self {
let message = e.to_string();
match e {
sqlx::Error::Database(db_err) => {
let (details, hint) = db_err
.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
.map(|pg_err| {
(
pg_err.detail().map(String::from),
pg_err.hint().map(String::from),
)
})
.unwrap_or((None, None));
DbError::database(
message,
DatabaseError {
code: db_err.code().map(|c| c.to_string()).unwrap_or_default(),
message: db_err.message().to_string(),
details,
hint,
constraint: db_err.constraint().map(|s| s.to_string()),
table: db_err.table().map(|s| s.to_string()),
column: None,
},
)
}
sqlx::Error::Io(_)
| sqlx::Error::Tls(_)
| sqlx::Error::PoolTimedOut
| sqlx::Error::PoolClosed
| sqlx::Error::WorkerCrashed => DbError::connection(message),
_ => DbError::other(message),
}
}
}