tank-postgres 0.32.0

Postgres driver implementation for Tank: the Rust data layer
Documentation
use crate::{
    PostgresDriver, PostgresPrepared, PostgresTransaction, ValueWrap,
    util::{
        postgres_type_to_value, stream_postgres_row_to_tank_row,
        stream_postgres_simple_query_message_to_tank_query_result, value_to_postgres_type,
    },
};
use async_stream::try_stream;
use openssl::ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode};
use postgres_openssl::MakeTlsConnector;
use postgres_types::ToSql;
use std::{
    borrow::Cow,
    env, mem,
    path::PathBuf,
    pin::{Pin, pin},
    str::FromStr,
};
use tank_core::{
    AsQuery, Connection, Driver, DynQuery, Entity, Error, ErrorContext, Executor, Query,
    QueryResult, RawQuery, Result, RowsAffected, SqlWriter, Transaction,
    future::Either,
    stream::{Stream, StreamExt, TryStreamExt},
    truncate_long,
};
use tokio::{spawn, task::JoinHandle};
use tokio_postgres::{NoTls, binary_copy::BinaryCopyInWriter};
use url::Url;

/// PostgreSQL connection.
#[derive(Debug)]
pub struct PostgresConnection {
    pub(crate) client: tokio_postgres::Client,
    pub(crate) handle: JoinHandle<()>,
    pub(crate) url: Url,
}

impl Executor for PostgresConnection {
    type Driver = PostgresDriver;

    async fn do_prepare(&mut self, sql: String) -> Result<Query<PostgresDriver>> {
        let sql = sql.as_str().trim_end().trim_end_matches(';');
        Ok(
            PostgresPrepared::new(self.client.prepare(&sql).await.map_err(|e| {
                let error = Error::new(e).context(format!(
                    "While preparing the query:\n{}",
                    truncate_long!(sql)
                ));
                log::error!("{:#}", error);
                error
            })?)
            .into(),
        )
    }

    fn run<'s>(
        &'s mut self,
        query: impl AsQuery<PostgresDriver> + 's,
    ) -> impl Stream<Item = Result<QueryResult>> + Send {
        let mut query = query.as_query();
        let context = format!("While running the query:\n{}", query.as_mut());
        let mut owned = mem::take(query.as_mut());
        match owned {
            Query::Raw(raw) => Either::Left(try_stream! {
                let sql = &raw.0;
                {
                    let stream = stream_postgres_simple_query_message_to_tank_query_result(
                        async move || self.client.simple_query_raw(sql).await.map_err(Into::into),
                    );
                    let mut stream = pin!(stream);
                    while let Some(value) = stream.next().await.transpose()? {
                        yield value;
                    }
                }
                *query.as_mut() = Query::Raw(raw);
            }),
            Query::Prepared(..) => Either::Right(try_stream! {
                let mut transaction = self.begin().await?;
                {
                    let mut stream = pin!(transaction.run(&mut owned));
                    while let Some(value) = stream.next().await.transpose()? {
                        yield value;
                    }
                }
                transaction.commit().await?;
                *query.as_mut() = mem::take(&mut owned);
            }),
        }
        .map_err(move |e: Error| {
            let error = e.context(context.clone());
            log::error!("{:#}", error);
            error
        })
    }

    fn fetch<'s>(
        &'s mut self,
        query: impl AsQuery<PostgresDriver> + 's,
    ) -> impl Stream<Item = Result<tank_core::Row>> + Send {
        let mut query = query.as_query();
        let context = format!("While fetching the query:\n{}", query.as_mut());
        let owned = mem::take(query.as_mut());
        stream_postgres_row_to_tank_row(async move || {
            let row_stream = match owned {
                Query::Raw(RawQuery(sql)) => {
                    let stream = self
                        .client
                        .query_raw(&sql, Vec::<ValueWrap>::new())
                        .await
                        .map_err(|e| Error::new(e).context(context.clone()))?;
                    *query.as_mut() = Query::raw(sql);
                    stream
                }
                Query::Prepared(mut prepared) => {
                    let mut params = prepared.take_params();
                    let types = prepared.statement.params();

                    for (i, param) in params.iter_mut().enumerate() {
                        *param = ValueWrap(Cow::Owned(
                            mem::take(param)
                                .0
                                .into_owned()
                                .try_as(&postgres_type_to_value(&types[i]))?,
                        ));
                    }
                    let stream = self
                        .client
                        .query_raw(&prepared.statement, params)
                        .await
                        .map_err(|e| Error::new(e).context(context.clone()))?;
                    *query.as_mut() = Query::Prepared(prepared);
                    stream
                }
            };
            Ok(row_stream).map_err(|e| {
                log::error!("{:#}", e);
                e
            })
        })
    }

    async fn append<'a, E, It>(&mut self, entities: It) -> Result<RowsAffected>
    where
        E: Entity + 'a,
        It: IntoIterator<Item = &'a E> + Send,
        <It as IntoIterator>::IntoIter: Send,
    {
        let writer = self.driver().sql_writer();
        let context = || {
            format!(
                "While appending to the table `{}`",
                E::table().full_name(writer.separator())
            )
        };
        let mut query = DynQuery::default();
        writer.write_copy::<E>(&mut query);
        let sink = match self
            .client
            .copy_in(&query.as_str() as &str)
            .await
            .with_context(context)
        {
            Ok(v) => v,
            Err(e) => {
                log::error!("{e:#}");
                return Err(e);
            }
        };
        let types: Vec<_> = E::columns()
            .into_iter()
            .map(|c| value_to_postgres_type(&c.value))
            .collect();
        let writer = BinaryCopyInWriter::new(sink, &types);
        let mut writer = pin!(writer);
        let columns_len = E::columns().len();
        let mut values = Vec::<ValueWrap>::with_capacity(columns_len);
        let mut refs = Vec::<&(dyn ToSql + Sync)>::with_capacity(columns_len);
        for entity in entities.into_iter() {
            values.extend(
                entity
                    .row_values()
                    .into_iter()
                    .map(|v| ValueWrap(Cow::Owned(v))),
            );
            refs.extend(
                values
                    .iter()
                    .map(|v| unsafe { &*(v as &(dyn ToSql + Sync) as *const _) }),
            );
            match Pin::as_mut(&mut writer)
                .write(&refs)
                .await
                .with_context(context)
            {
                Ok(_) => {}
                Err(e) => {
                    log::error!("{e:#}");
                    return Err(e);
                }
            };
            refs.clear();
            values.clear();
        }
        match writer.finish().await.with_context(context) {
            Ok(v) => Ok(RowsAffected {
                rows_affected: Some(v),
                last_affected_id: None,
            }),
            Err(e) => {
                log::error!("{e:#}");
                return Err(e);
            }
        }
    }
}

impl Connection for PostgresConnection {
    async fn connect(driver: &PostgresDriver, url: Cow<'static, str>) -> Result<Self> {
        let context = "While trying to connect to Postgres";
        let mut url = Self::sanitize_url(driver, url)?;
        let mut take_url_param = |key: &str, env_var: &str, remove: bool| {
            let value = url
                .query_pairs()
                .find_map(|(k, v)| if k == key { Some(v) } else { None })
                .map(|v| v.to_string());
            if remove && let Some(..) = value {
                let mut result = url.clone();
                result.set_query(None);
                result
                    .query_pairs_mut()
                    .extend_pairs(url.query_pairs().filter(|(k, _)| k != key));
                url = result;
            };
            value.or_else(|| env::var(env_var).ok().map(Into::into))
        };
        let sslmode = take_url_param("sslmode", "PGSSLMODE", false).unwrap_or("disable".into());
        let (client, handle) = if sslmode == "disable" {
            let (client, connection) = tokio_postgres::connect(url.as_str(), NoTls).await?;
            let handle = spawn(async move {
                if let Err(error) = connection.await
                    && !error.is_closed()
                {
                    log::error!("Postgres connection error: {:#?}", error);
                }
            });
            (client, handle)
        } else {
            let mut builder = SslConnector::builder(SslMethod::tls())?;
            let path = PathBuf::from_str(
                take_url_param("sslrootcert", "PGSSLROOTCERT", true)
                    .as_deref()
                    .unwrap_or("~/.postgresql/root.crt"),
            )
            .context(context)?;
            if path.exists() {
                builder.set_ca_file(path)?;
            }
            let path = PathBuf::from_str(
                take_url_param("sslcert", "PGSSLCERT", true)
                    .as_deref()
                    .unwrap_or("~/.postgresql/postgresql.crt"),
            )
            .context(context)?;
            if path.exists() {
                builder.set_certificate_chain_file(path)?;
            }
            let path = PathBuf::from_str(
                take_url_param("sslkey", "PGSSLKEY", true)
                    .as_deref()
                    .unwrap_or("~/.postgresql/postgresql.key"),
            )
            .context(context)?;
            if path.exists() {
                builder.set_private_key_file(path, SslFiletype::PEM)?;
            }
            builder.set_verify(SslVerifyMode::PEER);
            let connector = MakeTlsConnector::new(builder.build());
            let (client, connection) = tokio_postgres::connect(url.as_str(), connector).await?;
            let handle = spawn(async move {
                if let Err(error) = connection.await
                    && !error.is_closed()
                {
                    log::error!("Postgres connection error: {:#?}", error);
                }
            });
            (client, handle)
        };
        Ok(Self {
            client,
            handle,
            url,
        })
    }

    fn begin(&mut self) -> impl Future<Output = Result<PostgresTransaction<'_>>> {
        PostgresTransaction::new(self)
    }

    async fn duplicate(&self) -> Result<PostgresConnection>
    where
        Self: Sized,
    {
        Self::connect(&self.driver(), self.url.to_string().into()).await
    }

    async fn disconnect(self) -> Result<()> {
        drop(self.client);
        if let Err(e) = self.handle.await {
            let error = Error::new(e).context("While disconnecting from Postgres");
            log::error!("{:#}", error);
            return Err(error);
        }
        Ok(())
    }
}