use super::{BoxFuture, Db, DbError, FetchedThen, MaybeSend, MaybeSync, RawRows, Tx};
use crate::error::DatabaseError;
use postrust_sql::SqlParam;
use std::collections::HashSet;
use std::future::Future;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_postgres::error::SqlState;
use tokio_postgres::tls::TlsConnect;
use tokio_postgres::types::{FromSql, ToSql, Type};
use tokio_postgres::{Client, Row, Statement};
pub trait Connector: MaybeSend + MaybeSync {
type Stream: AsyncRead + AsyncWrite + Unpin + MaybeSend + 'static;
type Tls: TlsConnect<Self::Stream> + MaybeSend;
fn connect(&self) -> impl Future<Output = std::io::Result<Self::Stream>> + MaybeSend;
fn tls(&self) -> Self::Tls;
fn spawn(&self, connection: BoxFuture<'static, ()>);
fn sleep(&self, duration: Duration) -> impl Future<Output = ()> + MaybeSend {
let _ = duration;
std::future::pending()
}
}
pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct PgSocketDb<C> {
connector: C,
config: tokio_postgres::Config,
}
impl<C: Connector> PgSocketDb<C> {
pub fn new(connector: C, config: tokio_postgres::Config) -> Self {
Self { connector, config }
}
pub fn connector(&self) -> &C {
&self.connector
}
}
pub struct SocketTx {
client: Client,
closed: Arc<Mutex<Option<DbError>>>,
queued: Vec<(String, Vec<SqlParam>)>,
sent: Vec<(String, Vec<SqlParam>)>,
}
impl<C> Db for PgSocketDb<C>
where
C: Connector,
<C::Tls as TlsConnect<C::Stream>>::Stream: MaybeSend + 'static,
<C::Tls as TlsConnect<C::Stream>>::Future: MaybeSend,
{
type Tx<'a>
= SocketTx
where
Self: 'a;
async fn begin(&self) -> Result<SocketTx, DbError> {
let timeout = self
.config
.get_connect_timeout()
.copied()
.unwrap_or(DEFAULT_CONNECT_TIMEOUT);
let connect = std::pin::pin!(async {
let stream = self.connector.connect().await.map_err(|e| {
DbError::connection(format!("error connecting to the database: {e}"))
})?;
Ok::<_, DbError>(
self.config
.connect_raw(stream, self.connector.tls())
.await?,
)
});
let expired = std::pin::pin!(self.connector.sleep(timeout));
let (client, connection) = match futures::future::select(connect, expired).await {
futures::future::Either::Left((connected, _)) => connected?,
futures::future::Either::Right(_) => {
return Err(DbError::connection(format!(
"timed out after {timeout:?} connecting to the database"
)))
}
};
let closed = Arc::new(Mutex::new(None));
let died = closed.clone();
self.connector.spawn(Box::pin(async move {
let mut connection = std::pin::pin!(connection);
if let Err(e) = connection.as_mut().await {
if let Ok(mut died) = died.lock() {
*died = Some(DbError::from(e));
}
}
}));
Ok(SocketTx {
client,
closed,
queued: vec![("BEGIN".to_string(), Vec::new())],
sent: Vec::new(),
})
}
}
impl Tx for SocketTx {
async fn execute(&mut self, sql: &str, params: &[SqlParam]) -> Result<u64, DbError> {
let [(_, affected)] = self.send([(sql, params)]).await?;
Ok(affected)
}
async fn fetch_json(
&mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<serde_json::Value>, DbError> {
Ok(self
.query(sql, params)
.await?
.iter()
.map(row_to_json)
.collect())
}
async fn fetch_values(
&mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<Vec<serde_json::Value>>, DbError> {
Ok(self
.query(sql, params)
.await?
.iter()
.map(row_to_values)
.collect())
}
async fn fetch_raw(
&mut self,
sql: &str,
params: &[SqlParam],
count_column: &str,
) -> Result<RawRows, DbError> {
let rows = self.query(sql, params).await?;
let first = rows
.first()
.and_then(|row| row.try_get::<_, Raw>(0).ok()?.0.map(<[u8]>::to_vec));
let count = rows.first().and_then(|row| {
let index = row
.columns()
.iter()
.rposition(|c| c.name() == count_column)?;
match row.columns()[index].type_().oid() {
oid::INT8 => be_bytes(raw(row, index)?).map(i64::from_be_bytes),
_ => None,
}
});
Ok(RawRows::new(rows.len(), first, count))
}
async fn commit(mut self) -> Result<(), DbError> {
self.send([("COMMIT", &[][..])]).await?;
Ok(())
}
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, _), (then, _)] = self.send([(sql, params), (then, &[][..])]).await?;
Ok((
rows.iter().map(row_to_json).collect(),
then.iter().map(row_to_values).collect(),
))
}
async fn fetch_json_then_and_commit(
mut self,
sql: &str,
params: &[SqlParam],
then: &str,
) -> FetchedThen {
let [(rows, _), (then, _), _] = self
.send([(sql, params), (then, &[][..]), ("COMMIT", &[][..])])
.await?;
Ok((
rows.iter().map(row_to_json).collect(),
then.iter().map(row_to_values).collect(),
))
}
async fn fetch_json_and_commit(
mut self,
sql: &str,
params: &[SqlParam],
) -> Result<Vec<serde_json::Value>, DbError> {
let [(rows, _), _] = self.send([(sql, params), ("COMMIT", &[][..])]).await?;
Ok(rows.iter().map(row_to_json).collect())
}
}
impl SocketTx {
async fn query(&mut self, sql: &str, params: &[SqlParam]) -> Result<Vec<Row>, DbError> {
let [(rows, _)] = self.send([(sql, params)]).await?;
Ok(rows)
}
async fn send<const N: usize>(
&mut self,
statements: [(&str, &[SqlParam]); N],
) -> Result<[(Vec<Row>, u64); N], DbError> {
let mut ahead = std::mem::take(&mut self.queued);
for _ in 0..=ahead.len() + N + self.sent.len() {
match self.flights(&ahead, &statements).await {
Ok(answers) => {
self.sent.extend(ahead);
self.sent.extend(
statements
.iter()
.map(|(sql, params)| (sql.to_string(), params.to_vec())),
);
return answers
.try_into()
.map_err(|_| DbError::other("a pipelined statement went unanswered"));
}
Err(Failed::Error(error)) => return Err(error),
Err(Failed::Masked(sql, error)) => {
if !RESULT_TYPES.remember(&sql) {
return Err(error);
}
self.client
.batch_execute("ROLLBACK")
.await
.map_err(|e| self.error(e))?;
ahead = std::mem::take(&mut self.sent)
.into_iter()
.chain(ahead)
.collect();
}
}
}
Err(DbError::other(
"a statement's error was lost to a type lookup",
))
}
async fn flights(
&self,
ahead: &[(String, Vec<SqlParam>)],
statements: &[(&str, &[SqlParam])],
) -> Result<Vec<(Vec<Row>, u64)>, Failed> {
let all: Vec<(&str, &[SqlParam])> = ahead
.iter()
.map(|(sql, params)| (sql.as_str(), params.as_slice()))
.chain(statements.iter().copied())
.collect();
let prepare_first = |index: usize| RESULT_TYPES.must_prepare(all[index].0);
let mut answers = Vec::with_capacity(all.len());
let mut start = 0;
while start < all.len() {
let end = (start + 1..all.len())
.find(|&index| prepare_first(index))
.unwrap_or(all.len());
let queued = |index: usize, e: DbError| match index < ahead.len() {
true => e.queued(),
false => e,
};
let mut prepared = match prepare_first(start) {
true => Some(
self.prepare(all[start].0, all[start].1)
.await
.map_err(|e| Failed::Error(queued(start, e)))?,
),
false => None,
};
let was_prepared = prepared.is_some();
let client = &self.client;
let futures: Vec<_> = all[start..end]
.iter()
.map(|(sql, params)| Box::pin(run(client, sql, params, prepared.take())))
.collect();
for (offset, answer) in in_order(futures).await.into_iter().enumerate() {
match answer {
Ok(answer) => answers.push(answer),
Err(e)
if e.code() == Some(&SqlState::IN_FAILED_SQL_TRANSACTION)
&& !(offset == 0 && was_prepared) =>
{
let sql = all[start + offset].0.to_string();
return Err(Failed::Masked(sql, queued(start + offset, self.error(e))));
}
Err(e) => return Err(Failed::Error(queued(start + offset, self.error(e)))),
}
}
start = end;
}
Ok(answers.split_off(ahead.len()))
}
async fn prepare(&self, sql: &str, params: &[SqlParam]) -> Result<Statement, DbError> {
let types: Vec<Type> = Bound::new(params).iter().map(|(_, ty)| ty).collect();
self.client
.prepare_typed(sql, &types)
.await
.map_err(|e| self.error(e))
}
fn error(&self, e: tokio_postgres::Error) -> DbError {
if e.is_closed() {
if let Some(why) = self.closed.lock().ok().and_then(|mut why| why.take()) {
return why;
}
}
e.into()
}
}
enum Failed {
Error(DbError),
Masked(String, DbError),
}
struct ResultTypes(Mutex<HashSet<Box<str>>>);
const RESULT_TYPES_KEPT: usize = 1024;
static RESULT_TYPES: LazyLock<ResultTypes> =
LazyLock::new(|| ResultTypes(Mutex::new(HashSet::new())));
impl ResultTypes {
fn must_prepare(&self, sql: &str) -> bool {
self.0.lock().is_ok_and(|known| known.contains(sql))
}
fn remember(&self, sql: &str) -> bool {
let Ok(mut known) = self.0.lock() else {
return false;
};
if known.contains(sql) {
return false;
}
if known.len() >= RESULT_TYPES_KEPT {
known.clear();
}
known.insert(sql.into())
}
}
async fn run(
client: &Client,
sql: &str,
params: &[SqlParam],
prepared: Option<Statement>,
) -> Result<(Vec<Row>, u64), tokio_postgres::Error> {
let bound = Bound::new(params);
let stream = match &prepared {
Some(statement) => {
client
.query_raw(statement, bound.iter().map(|(value, _)| value))
.await?
}
None => client.query_typed_raw(sql, bound.iter()).await?,
};
futures::pin_mut!(stream);
let mut rows = Vec::new();
while let Some(row) = futures::StreamExt::next(&mut stream).await.transpose()? {
rows.push(row);
}
Ok((rows, stream.rows_affected().unwrap_or_default()))
}
async fn in_order<F, T, E>(futures: Vec<F>) -> Vec<Result<T, E>>
where
F: Future<Output = Result<T, E>> + Unpin,
{
struct Slot<F, O> {
future: F,
started: bool,
output: Option<O>,
}
let mut slots: Vec<Slot<F, Result<T, E>>> = futures
.into_iter()
.map(|future| Slot {
future,
started: false,
output: None,
})
.collect();
std::future::poll_fn(|cx| {
let mut pending = false;
let mut failed = false;
for slot in slots.iter_mut() {
if let Some(output) = &slot.output {
failed |= output.is_err();
continue;
}
if failed && !slot.started {
continue;
}
slot.started = true;
match std::pin::Pin::new(&mut slot.future).poll(cx) {
std::task::Poll::Ready(value) => {
failed |= value.is_err();
slot.output = Some(value);
}
std::task::Poll::Pending => pending = true,
}
}
match pending {
true => std::task::Poll::Pending,
false => std::task::Poll::Ready(()),
}
})
.await;
slots.into_iter().map_while(|slot| slot.output).collect()
}
struct Bound(Vec<(Box<dyn ToSql + Sync + Send>, Type)>);
impl Bound {
fn new(params: &[SqlParam]) -> Self {
Self(params.iter().map(bind).collect())
}
fn iter(&self) -> impl ExactSizeIterator<Item = (&(dyn ToSql + Sync), Type)> {
self.0
.iter()
.map(|(value, ty)| (&**value as &(dyn ToSql + Sync), ty.clone()))
}
}
fn bind(param: &SqlParam) -> (Box<dyn ToSql + Sync + Send>, Type) {
match param {
SqlParam::Null => (Box::new(None::<String>), Type::TEXT),
SqlParam::Bool(b) => (Box::new(*b), Type::BOOL),
SqlParam::Int(n) => (Box::new(*n), Type::INT8),
SqlParam::Float(f) => (Box::new(*f), Type::FLOAT8),
SqlParam::Text(s) => (Box::new(s.clone()), Type::TEXT),
SqlParam::Bytes(b) => (Box::new(b.clone()), Type::BYTEA),
SqlParam::Json(j) => (Box::new(j.clone()), Type::JSONB),
SqlParam::Uuid(u) => (Box::new(*u), Type::UUID),
SqlParam::Timestamp(t) => (Box::new(*t), Type::TIMESTAMPTZ),
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();
(Box::new(strings), Type::TEXT_ARRAY)
}
}
}
struct Raw<'a>(Option<&'a [u8]>);
impl<'a> FromSql<'a> for Raw<'a> {
fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
Ok(Raw(Some(raw)))
}
fn from_sql_null(_: &Type) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
Ok(Raw(None))
}
fn accepts(_: &Type) -> bool {
true
}
}
fn raw(row: &Row, index: usize) -> Option<&[u8]> {
row.try_get::<_, Raw>(index).ok()?.0
}
fn be_bytes<const N: usize>(bytes: &[u8]) -> Option<[u8; N]> {
bytes.try_into().ok()
}
fn row_to_json(row: &Row) -> serde_json::Value {
let columns = row.columns();
let mut map = serde_json::Map::new();
for column in columns {
let index = columns
.iter()
.rposition(|c| c.name() == column.name())
.unwrap_or_default();
let value = decode(columns[index].type_(), raw(row, index));
map.insert(column.name().to_string(), value);
}
serde_json::Value::Object(map)
}
fn row_to_values(row: &Row) -> Vec<serde_json::Value> {
row.columns()
.iter()
.enumerate()
.map(|(index, column)| decode(column.type_(), raw(row, index)))
.collect()
}
fn decode(ty: &Type, raw: Option<&[u8]>) -> serde_json::Value {
use serde_json::Value;
let Some(raw) = raw else {
return Value::Null;
};
let number = |n: serde_json::Number| Some(Value::Number(n));
let value = match ty.oid() {
oid::INT2 => be_bytes(raw)
.map(i16::from_be_bytes)
.and_then(|v| number(v.into())),
oid::INT4 => be_bytes(raw)
.map(i32::from_be_bytes)
.and_then(|v| number(v.into())),
oid::INT8 => be_bytes(raw)
.map(i64::from_be_bytes)
.and_then(|v| number(v.into())),
oid::FLOAT4 => be_bytes(raw)
.map(f32::from_be_bytes)
.and_then(|v| serde_json::Number::from_f64(v as f64))
.map(Value::Number),
oid::FLOAT8 => be_bytes(raw)
.map(f64::from_be_bytes)
.and_then(serde_json::Number::from_f64)
.map(Value::Number),
oid::NUMERIC => numeric(raw).map(|text| {
serde_json::from_str::<Value>(&text)
.ok()
.filter(Value::is_number)
.unwrap_or(Value::String(text))
}),
oid::BOOL => match raw {
[b] => Some(Value::Bool(*b != 0)),
_ => None,
},
oid::JSON => serde_json::from_slice(raw).ok(),
oid::JSONB => match raw.split_first() {
Some((1, text)) => serde_json::from_slice(text).ok(),
_ => None,
},
oid::UUID => uuid::Uuid::from_slice(raw)
.ok()
.map(|v| Value::String(v.to_string())),
oid::TIMESTAMPTZ => timestamp(raw).map(|v| Value::String(v.and_utc().to_rfc3339())),
oid::TIMESTAMP => {
timestamp(raw).map(|v| Value::String(v.format("%Y-%m-%dT%H:%M:%S%.f").to_string()))
}
oid::DATE => be_bytes(raw)
.map(i32::from_be_bytes)
.and_then(|days| {
epoch()
.date()
.checked_add_signed(chrono::TimeDelta::try_days(days.into())?)
})
.map(|v| Value::String(v.to_string())),
oid::TIME => be_bytes(raw)
.map(i64::from_be_bytes)
.map(|us| {
chrono::NaiveTime::default()
.overflowing_add_signed(chrono::TimeDelta::microseconds(us))
.0
})
.map(|v| Value::String(v.to_string())),
oid::TEXT | oid::NAME | oid::BPCHAR | oid::VARCHAR | oid::UNKNOWN => {
std::str::from_utf8(raw)
.ok()
.map(|s| Value::String(s.to_string()))
}
_ if ty.name() == "citext" => std::str::from_utf8(raw)
.ok()
.map(|s| Value::String(s.to_string())),
_ => None,
};
value.unwrap_or(Value::Null)
}
mod oid {
pub const BOOL: u32 = 16;
pub const NAME: u32 = 19;
pub const INT8: u32 = 20;
pub const INT2: u32 = 21;
pub const INT4: u32 = 23;
pub const TEXT: u32 = 25;
pub const JSON: u32 = 114;
pub const FLOAT4: u32 = 700;
pub const FLOAT8: u32 = 701;
pub const UNKNOWN: u32 = 705;
pub const BPCHAR: u32 = 1042;
pub const VARCHAR: u32 = 1043;
pub const DATE: u32 = 1082;
pub const TIME: u32 = 1083;
pub const TIMESTAMP: u32 = 1114;
pub const TIMESTAMPTZ: u32 = 1184;
pub const NUMERIC: u32 = 1700;
pub const UUID: u32 = 2950;
pub const JSONB: u32 = 3802;
}
fn epoch() -> chrono::NaiveDateTime {
chrono::NaiveDate::from_ymd_opt(2000, 1, 1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.unwrap_or_default()
}
fn timestamp(raw: &[u8]) -> Option<chrono::NaiveDateTime> {
let us = i64::from_be_bytes(be_bytes(raw)?);
epoch().checked_add_signed(chrono::TimeDelta::microseconds(us))
}
fn numeric(raw: &[u8]) -> Option<String> {
use bigdecimal::num_bigint::{BigInt, Sign};
use bigdecimal::BigDecimal;
let word =
|i: usize| -> Option<u16> { Some(u16::from_be_bytes(be_bytes(raw.get(i..i + 2)?)?)) };
let ndigits = word(0)? as usize;
let weight = word(2)? as i16;
let sign = word(4)?;
if sign != 0x0000 && sign != 0x4000 {
return None;
}
let digits: Vec<u16> = (0..ndigits)
.map(|i| word(8 + i * 2))
.collect::<Option<_>>()?;
if digits.is_empty() {
return Some(BigDecimal::from(0u64).to_string());
}
let mut cents = Vec::with_capacity(digits.len() * 2);
for digit in &digits {
if *digit >= 10000 {
return None;
}
cents.push((digit / 100) as u8);
cents.push((digit % 100) as u8);
}
let sign = if sign == 0x4000 {
Sign::Minus
} else {
Sign::Plus
};
let scale = (digits.len() as i64 - weight as i64 - 1) * 4;
let bigint = BigInt::from_radix_be(sign, ¢s, 100)?;
Some(BigDecimal::new(bigint, scale).to_string())
}
impl From<tokio_postgres::Error> for DbError {
fn from(e: tokio_postgres::Error) -> Self {
let message = match e.as_db_error() {
Some(db) => format!("error returned from database: {}", db.message()),
None => {
let mut message = e.to_string();
let mut source = std::error::Error::source(&e);
while let Some(cause) = source {
message.push_str(": ");
message.push_str(&cause.to_string());
source = cause.source();
}
message
}
};
match e.as_db_error() {
Some(db) => DbError::database(
message,
DatabaseError {
code: db.code().code().to_string(),
message: db.message().to_string(),
details: db.detail().map(String::from),
hint: db.hint().map(String::from),
constraint: db.constraint().map(String::from),
table: db.table().map(String::from),
column: None,
},
),
None if e.is_closed() || io_error(&e) => DbError::connection(message),
None => DbError::other(message),
}
}
}
fn io_error(e: &tokio_postgres::Error) -> bool {
let mut source = std::error::Error::source(e);
while let Some(cause) = source {
if cause.is::<std::io::Error>() {
return true;
}
source = cause.source();
}
false
}