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::{AssertSqlSafe, 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            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(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
131impl crate::session::Session<'_> {
132    /// Run this session's statements on a sqlx connection, in order.
133    ///
134    /// For code that holds a sqlx transaction of its own rather than a
135    /// [`Tx`] -- GraphQL's resolvers, a hand-written route -- and has to make
136    /// it the caller's. Pass the transaction (`&mut *tx`): every setting is
137    /// transaction-local, so it is gone when the transaction ends and the
138    /// connection goes back to the pool as it came. On a connection outside
139    /// a transaction each statement would be its own, and nothing would stay.
140    ///
141    /// Parameters are bound as [`SqlxTx`] binds them, so a session means the
142    /// same thing to both. A failure here is reported as a queued statement's,
143    /// as it is when [`Tx::queue`] runs the same statements.
144    pub async fn apply_sqlx(&self, conn: &mut sqlx::PgConnection) -> Result<(), DbError> {
145        for statement in self.statements() {
146            query(&statement.sql, &statement.params)
147                .execute(&mut *conn)
148                .await
149                .map_err(|e| DbError::from(e).queued())?;
150        }
151        Ok(())
152    }
153}
154
155async fn fetch_all(
156    tx: &mut sqlx::Transaction<'static, Postgres>,
157    sql: &str,
158    params: &[SqlParam],
159) -> Result<Vec<PgRow>, sqlx::Error> {
160    query(sql, params).fetch_all(&mut **tx).await
161}
162
163/// The statement to run: `sql`, with each parameter bound with the type sqlx
164/// gives its Rust value, and the SQL's own casts deciding the rest.
165///
166/// Every statement this driver runs comes through here, which is why the
167/// driver wraps its SQL in `AssertSqlSafe` here and nowhere else. sqlx 0.9
168/// accepts only a `&'static str` as SQL unless told otherwise, as a speed bump
169/// against `format!`-ing user input into a query. The query builder in
170/// `crate::query` assembles statements at runtime, but every value a request
171/// carries arrives in `params` and is bound, never spliced in. What is spliced
172/// in is SQL the builder wrote itself; identifiers, each quoted by
173/// `postrust_sql::escape_ident`; literals the builder quotes itself, by
174/// `postrust_sql::quote_literal`; and the type name of a `::` cast in a
175/// select, which the parser admits only as a single type name -- letters,
176/// digits, `_` and `$` -- or one of the SQL standard's multi-word names, in
177/// the standard's spelling (see `parse_cast_type` in
178/// `crate::api_request::query_params`). The assertion is that, and nothing
179/// more. The schema cache's two runtime statements, in
180/// `schema_cache::queries::enum_values`, quote their identifiers the same way
181/// and make the same assertion for themselves.
182fn query(sql: &str, params: &[SqlParam]) -> Query<'static, Postgres, PgArguments> {
183    let mut query = sqlx::query(AssertSqlSafe(sql));
184    for param in params {
185        query = match param {
186            SqlParam::Null => query.bind(None::<String>),
187            SqlParam::Bool(b) => query.bind(b),
188            SqlParam::Int(n) => query.bind(n),
189            SqlParam::Float(f) => query.bind(f),
190            SqlParam::Text(s) => query.bind(s),
191            SqlParam::Bytes(b) => query.bind(b),
192            SqlParam::Json(j) => query.bind(j),
193            SqlParam::Uuid(u) => query.bind(u),
194            SqlParam::Timestamp(t) => query.bind(t),
195            SqlParam::Array(arr) => {
196                // Convert array to Vec<String> for text arrays
197                let strings: Vec<String> = arr
198                    .iter()
199                    .map(|p| match p {
200                        SqlParam::Text(s) => s.clone(),
201                        SqlParam::Int(n) => n.to_string(),
202                        SqlParam::Bool(b) => b.to_string(),
203                        other => format!("{:?}", other),
204                    })
205                    .collect();
206                query.bind(strings)
207            }
208        };
209    }
210
211    query
212}
213
214impl From<sqlx::Error> for DbError {
215    fn from(e: sqlx::Error) -> Self {
216        let message = e.to_string();
217        match e {
218            sqlx::Error::Database(db_err) => {
219                // Phrased here rather than taken from sqlx's `Display`, which
220                // from 0.9 appends " at line N" -- the line of PostgreSQL's
221                // own C source that raised the error (its `L` field), not a
222                // line of the statement. It means nothing to whoever reads the
223                // message, differs between PostgreSQL builds, and is not what
224                // the socket driver says. This is the 0.8 wording, word for
225                // word.
226                let message = format!("error returned from database: {}", db_err.message());
227                // Try to downcast to Postgres-specific error for additional details
228                let (details, hint) = db_err
229                    .try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
230                    .map(|pg_err| {
231                        (
232                            pg_err.detail().map(String::from),
233                            pg_err.hint().map(String::from),
234                        )
235                    })
236                    .unwrap_or((None, None));
237
238                DbError::database(
239                    message,
240                    DatabaseError {
241                        code: db_err.code().map(|c| c.to_string()).unwrap_or_default(),
242                        message: db_err.message().to_string(),
243                        details,
244                        hint,
245                        constraint: db_err.constraint().map(|s| s.to_string()),
246                        table: db_err.table().map(|s| s.to_string()),
247                        column: None,
248                    },
249                )
250            }
251            // The connection, not a statement on it: it broke, it could not be
252            // had from the pool in time, or the pool is gone.
253            sqlx::Error::Io(_)
254            | sqlx::Error::Tls(_)
255            | sqlx::Error::PoolTimedOut
256            | sqlx::Error::PoolClosed
257            | sqlx::Error::WorkerCrashed => DbError::connection(message),
258            _ => DbError::other(message),
259        }
260    }
261}