Skip to main content

postrust_core/db/
sqlx_impl.rs

1//! [`Db`] over a sqlx pool: the driver the server and the Lambda adapter use.
2
3use super::{Db, DbError, FetchedThen, RawRows, Tx};
4use crate::error::DatabaseError;
5use crate::row_json::{row_to_json, row_to_values};
6use postrust_sql::SqlParam;
7use sqlx::postgres::{PgArguments, PgPool, PgRow};
8use sqlx::query::Query;
9use sqlx::{Postgres, Row, ValueRef};
10
11/// A transaction on a pooled connection, which goes back to the pool when this
12/// is committed or dropped.
13///
14/// sqlx cannot pipeline, so [`Tx::queue`]d statements run one after another
15/// ahead of the next statement -- the round trips they always took. On a pool
16/// next to the database each is a fraction of a millisecond.
17pub struct SqlxTx {
18    tx: sqlx::Transaction<'static, Postgres>,
19    queued: Vec<(String, Vec<SqlParam>)>,
20}
21
22impl Db for PgPool {
23    type Tx<'a> = SqlxTx;
24
25    async fn begin(&self) -> Result<SqlxTx, DbError> {
26        Ok(SqlxTx {
27            tx: PgPool::begin(self).await?,
28            queued: Vec::new(),
29        })
30    }
31}
32
33impl SqlxTx {
34    /// Run what was queued, in order.
35    async fn flush(&mut self) -> Result<(), DbError> {
36        for (sql, params) in std::mem::take(&mut self.queued) {
37            bind(sqlx::query(&sql), &params)
38                .execute(&mut *self.tx)
39                .await
40                .map_err(|e| DbError::from(e).queued())?;
41        }
42        Ok(())
43    }
44}
45
46impl Tx for SqlxTx {
47    async fn execute(&mut self, sql: &str, params: &[SqlParam]) -> Result<u64, DbError> {
48        self.flush().await?;
49        Ok(bind(sqlx::query(sql), params)
50            .execute(&mut *self.tx)
51            .await?
52            .rows_affected())
53    }
54
55    async fn fetch_json(
56        &mut self,
57        sql: &str,
58        params: &[SqlParam],
59    ) -> Result<Vec<serde_json::Value>, DbError> {
60        // `into_iter` matters for large result sets: each row's buffers are
61        // freed as soon as it has been converted, rather than the whole
62        // `Vec<PgRow>` staying alive alongside the whole `Vec<Value>`.
63        self.flush().await?;
64        Ok(fetch_all(&mut self.tx, sql, params)
65            .await?
66            .into_iter()
67            .map(|row| row_to_json(&row))
68            .collect())
69    }
70
71    async fn fetch_values(
72        &mut self,
73        sql: &str,
74        params: &[SqlParam],
75    ) -> Result<Vec<Vec<serde_json::Value>>, DbError> {
76        self.flush().await?;
77        Ok(fetch_all(&mut self.tx, sql, params)
78            .await?
79            .into_iter()
80            .map(|row| row_to_values(&row))
81            .collect())
82    }
83
84    async fn fetch_raw(
85        &mut self,
86        sql: &str,
87        params: &[SqlParam],
88        count_column: &str,
89    ) -> Result<RawRows, DbError> {
90        self.flush().await?;
91        let rows = fetch_all(&mut self.tx, sql, params).await?;
92        let first = rows.first().and_then(|row| {
93            let value = row.try_get_raw(0).ok()?;
94            if value.is_null() {
95                return None;
96            }
97            Some(value.as_bytes().ok()?.to_vec())
98        });
99        let count = rows
100            .first()
101            .and_then(|row| row.try_get::<i64, _>(count_column).ok());
102        Ok(RawRows::new(rows.len(), first, count))
103    }
104
105    async fn commit(mut self) -> Result<(), DbError> {
106        self.flush().await?;
107        Ok(self.tx.commit().await?)
108    }
109
110    fn queue(&mut self, sql: &str, params: &[SqlParam]) {
111        self.queued.push((sql.to_string(), params.to_vec()));
112    }
113
114    async fn fetch_json_then(&mut self, sql: &str, params: &[SqlParam], then: &str) -> FetchedThen {
115        let rows = self.fetch_json(sql, params).await?;
116        let then = self.fetch_values(then, &[]).await?;
117        Ok((rows, then))
118    }
119
120    async fn fetch_json_and_commit(
121        mut self,
122        sql: &str,
123        params: &[SqlParam],
124    ) -> Result<Vec<serde_json::Value>, DbError> {
125        let rows = self.fetch_json(sql, params).await?;
126        self.commit().await?;
127        Ok(rows)
128    }
129}
130
131async fn fetch_all(
132    tx: &mut sqlx::Transaction<'static, Postgres>,
133    sql: &str,
134    params: &[SqlParam],
135) -> Result<Vec<PgRow>, sqlx::Error> {
136    bind(sqlx::query(sql), params).fetch_all(&mut **tx).await
137}
138
139/// Bind each parameter with the type sqlx gives its Rust value, and let the
140/// SQL's own casts decide the rest.
141fn bind<'q>(
142    mut query: Query<'q, Postgres, PgArguments>,
143    params: &'q [SqlParam],
144) -> Query<'q, Postgres, PgArguments> {
145    for param in params {
146        query = match param {
147            SqlParam::Null => query.bind(None::<String>),
148            SqlParam::Bool(b) => query.bind(b),
149            SqlParam::Int(n) => query.bind(n),
150            SqlParam::Float(f) => query.bind(f),
151            SqlParam::Text(s) => query.bind(s),
152            SqlParam::Bytes(b) => query.bind(b),
153            SqlParam::Json(j) => query.bind(j),
154            SqlParam::Uuid(u) => query.bind(u),
155            SqlParam::Timestamp(t) => query.bind(t),
156            SqlParam::Array(arr) => {
157                // Convert array to Vec<String> for text arrays
158                let strings: Vec<String> = arr
159                    .iter()
160                    .map(|p| match p {
161                        SqlParam::Text(s) => s.clone(),
162                        SqlParam::Int(n) => n.to_string(),
163                        SqlParam::Bool(b) => b.to_string(),
164                        other => format!("{:?}", other),
165                    })
166                    .collect();
167                query.bind(strings)
168            }
169        };
170    }
171
172    query
173}
174
175impl From<sqlx::Error> for DbError {
176    fn from(e: sqlx::Error) -> Self {
177        let message = e.to_string();
178        match e {
179            sqlx::Error::Database(db_err) => {
180                // Try to downcast to Postgres-specific error for additional details
181                let (details, hint) = db_err
182                    .try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
183                    .map(|pg_err| {
184                        (
185                            pg_err.detail().map(String::from),
186                            pg_err.hint().map(String::from),
187                        )
188                    })
189                    .unwrap_or((None, None));
190
191                DbError::database(
192                    message,
193                    DatabaseError {
194                        code: db_err.code().map(|c| c.to_string()).unwrap_or_default(),
195                        message: db_err.message().to_string(),
196                        details,
197                        hint,
198                        constraint: db_err.constraint().map(|s| s.to_string()),
199                        table: db_err.table().map(|s| s.to_string()),
200                        column: None,
201                    },
202                )
203            }
204            // The connection, not a statement on it: it broke, it could not be
205            // had from the pool in time, or the pool is gone.
206            sqlx::Error::Io(_)
207            | sqlx::Error::Tls(_)
208            | sqlx::Error::PoolTimedOut
209            | sqlx::Error::PoolClosed
210            | sqlx::Error::WorkerCrashed => DbError::connection(message),
211            _ => DbError::other(message),
212        }
213    }
214}