Skip to main content

keelson_sqlx/
psql.rs

1//! The PostgreSQL driver: [`Pool`] implements
2//! [`Executor`], [`Begin`], [`BeginWith`] and [`StreamExecutor`].
3
4use std::sync::Arc;
5
6use keelson_core::Value;
7use keelson_exec::{
8    Begin, BeginWith, Column, ExecError, ExecFuture, ExecResult, Executor, Family, RawConnection,
9    Row, RowStream, Statement, StreamExecutor, Transaction, TxConflict, TxConflictError, TxOptions,
10};
11use sqlx::postgres::{PgArgumentBuffer, PgArguments, PgRow, PgTypeInfo};
12use sqlx::{Column as _, Postgres, Row as _, TypeInfo as _, ValueRef as _};
13
14use crate::common::{decode_err, unhandled};
15
16/// A PostgreSQL connection pool. sqlx's pool is the pool; this adds nothing
17/// but the keelson traits.
18#[derive(Debug, Clone)]
19pub struct Pool {
20    inner: sqlx::PgPool,
21}
22
23impl Pool {
24    /// Connect to `url` (`postgres://user:pass@host:port/db`).
25    pub async fn connect(url: &str) -> Result<Self, ExecError> {
26        let inner = sqlx::postgres::PgPoolOptions::new()
27            .connect(url)
28            .await
29            .map_err(ExecError::driver)?;
30        Ok(Pool { inner })
31    }
32
33    /// Wrap an existing sqlx pool.
34    pub fn from_pool(inner: sqlx::PgPool) -> Self {
35        Pool { inner }
36    }
37
38    /// The wrapped sqlx pool — keelson is a layer, not a jail.
39    pub fn inner(&self) -> &sqlx::PgPool {
40        &self.inner
41    }
42}
43
44impl Executor for Pool {
45    fn family(&self) -> Family {
46        Family::Postgres
47    }
48
49    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
50        Box::pin(async move {
51            let Statement { sql, args, .. } = stmt;
52            do_fetch(&self.inner, &sql, args).await
53        })
54    }
55
56    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
57        Box::pin(async move {
58            let Statement { sql, args, .. } = stmt;
59            do_execute(&self.inner, &sql, args).await
60        })
61    }
62}
63
64impl Begin for Pool {
65    fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>> {
66        Box::pin(async move {
67            let conn = self.inner.acquire().await.map_err(ExecError::driver)?;
68            Transaction::begin_on(Box::new(RawConn { conn })).await
69        })
70    }
71}
72
73impl BeginWith for Pool {
74    fn begin_with(&self, opts: TxOptions) -> ExecFuture<'_, Result<Transaction, ExecError>> {
75        Box::pin(async move {
76            // Refuse before taking a connection out of the pool: an
77            // unsupported option costs nothing and disturbs nothing.
78            opts.check(Family::Postgres)?;
79            let conn = self.inner.acquire().await.map_err(ExecError::driver)?;
80            Transaction::begin_on_with(Box::new(RawConn { conn }), opts).await
81        })
82    }
83}
84
85impl StreamExecutor for Pool {
86    fn fetch_stream(&self, stmt: Statement) -> ExecFuture<'_, Result<RowStream, ExecError>> {
87        Box::pin(async move {
88            let pool = self.inner.clone();
89            let (tx, rx) = tokio::sync::mpsc::channel::<Result<Row, ExecError>>(64);
90            tokio::spawn(async move {
91                use futures_util::StreamExt as _;
92                let Statement { sql, args, .. } = stmt;
93                let q = match bind_args(&sql, args) {
94                    Ok(q) => q,
95                    Err(e) => {
96                        let _ = tx.send(Err(e)).await;
97                        return;
98                    }
99                };
100                let mut native = q.fetch(&pool);
101                let mut header: Option<Arc<[Column]>> = None;
102                while let Some(next) = native.next().await {
103                    let msg = match next {
104                        Ok(row) => decode_row(&row, &mut header),
105                        Err(e) => Err(ExecError::driver(e)),
106                    };
107                    let stop = msg.is_err();
108                    if tx.send(msg).await.is_err() || stop {
109                        return;
110                    }
111                }
112            });
113            Ok(RowStream::new(rx))
114        })
115    }
116}
117
118/// One checked-out connection, exclusively held by a [`Transaction`].
119#[derive(Debug)]
120struct RawConn {
121    conn: sqlx::pool::PoolConnection<Postgres>,
122}
123
124impl RawConnection for RawConn {
125    fn family(&self) -> Family {
126        Family::Postgres
127    }
128
129    fn fetch<'a>(
130        &'a mut self,
131        sql: &'a str,
132        args: Vec<Value>,
133    ) -> ExecFuture<'a, Result<Vec<Row>, ExecError>> {
134        Box::pin(async move { do_fetch(&mut *self.conn, sql, args).await })
135    }
136
137    fn execute<'a>(
138        &'a mut self,
139        sql: &'a str,
140        args: Vec<Value>,
141    ) -> ExecFuture<'a, Result<ExecResult, ExecError>> {
142        Box::pin(async move { do_execute(&mut *self.conn, sql, args).await })
143    }
144
145    fn abandon(self: Box<Self>) {
146        // Detached: not returned to the pool. Dropping the raw connection
147        // closes it, and the server discards the open transaction.
148        let _ = self.conn.detach();
149    }
150}
151
152/// A driver failure, with concurrency conflicts classified out of it.
153///
154/// PostgreSQL reports them as SQLSTATEs, so this is a table lookup rather
155/// than message matching: `40001` serialization_failure, `40P01`
156/// deadlock_detected, `55P03` lock_not_available (what `lock_timeout` and
157/// `NOWAIT` raise). Everything else stays an opaque driver error.
158fn driver_err(e: sqlx::Error) -> ExecError {
159    if let sqlx::Error::Database(db) = &e {
160        let kind = match db.code().as_deref() {
161            Some("40001") => Some(TxConflict::Serialization),
162            Some("40P01") => Some(TxConflict::Deadlock),
163            Some("55P03") => Some(TxConflict::LockTimeout),
164            _ => None,
165        };
166        if let Some(kind) = kind {
167            let code = db.code().unwrap_or_default().into_owned();
168            let message = db.message().to_owned();
169            return TxConflictError::new(kind, code, message)
170                .with_source(e)
171                .into_exec_error();
172        }
173    }
174    ExecError::driver(e)
175}
176
177async fn do_fetch<'e, E>(exec: E, sql: &str, args: Vec<Value>) -> Result<Vec<Row>, ExecError>
178where
179    E: sqlx::Executor<'e, Database = Postgres>,
180{
181    let rows = bind_args(sql, args)?
182        .fetch_all(exec)
183        .await
184        .map_err(driver_err)?;
185    let mut header: Option<Arc<[Column]>> = None;
186    rows.iter().map(|r| decode_row(r, &mut header)).collect()
187}
188
189async fn do_execute<'e, E>(exec: E, sql: &str, args: Vec<Value>) -> Result<ExecResult, ExecError>
190where
191    E: sqlx::Executor<'e, Database = Postgres>,
192{
193    // Zero-argument statements go over the driver's plain (unprepared) path:
194    // MySQL refuses transaction control (`BEGIN`, `SAVEPOINT …`) in the
195    // prepared-statement protocol, and nothing is gained by preparing an
196    // argument-less statement anyway.
197    let done = if args.is_empty() {
198        exec.execute(sql).await.map_err(driver_err)?
199    } else {
200        bind_args(sql, args)?
201            .execute(exec)
202            .await
203            .map_err(driver_err)?
204    };
205    // PostgreSQL has no last-insert-id; RETURNING is the honest story.
206    Ok(ExecResult::new(done.rows_affected(), None))
207}
208
209/// An untyped SQL `NULL`: parameter OID `unknown`, so the server infers the
210/// type from context. `Value::Null` carries no type, and a typed null (say,
211/// `text`) would be refused where an `int` is expected.
212#[derive(Debug)]
213struct UnknownNull;
214
215impl sqlx::Type<Postgres> for UnknownNull {
216    fn type_info() -> PgTypeInfo {
217        PgTypeInfo::with_name("unknown")
218    }
219}
220
221impl sqlx::Encode<'_, Postgres> for UnknownNull {
222    fn encode_by_ref(
223        &self,
224        _buf: &mut PgArgumentBuffer,
225    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
226        Ok(sqlx::encode::IsNull::Yes)
227    }
228}
229
230/// The total map `Value` → PostgreSQL parameter, per the "binds as" column of
231/// `docs/type-mappings.md`: native parameter types throughout (this driver
232/// has them all).
233fn bind_args<'q>(
234    sql: &'q str,
235    args: Vec<Value>,
236) -> Result<sqlx::query::Query<'q, Postgres, PgArguments>, ExecError> {
237    let mut q = sqlx::query(sql);
238    for v in args {
239        q = match v {
240            Value::Null => q.bind(UnknownNull),
241            Value::Bool(x) => q.bind(x),
242            // PostgreSQL has no 1-byte integer; widen. Unsigned widths widen
243            // into the next signed size, and u64 must fit in i64 or is
244            // refused loudly.
245            Value::I8(x) => q.bind(i16::from(x)),
246            Value::I16(x) => q.bind(x),
247            Value::I32(x) => q.bind(x),
248            Value::I64(x) => q.bind(x),
249            Value::U8(x) => q.bind(i16::from(x)),
250            Value::U16(x) => q.bind(i32::from(x)),
251            Value::U32(x) => q.bind(i64::from(x)),
252            Value::U64(x) => {
253                q.bind(i64::try_from(x).map_err(|_| unsupported_value("u64 out of i64 range"))?)
254            }
255            Value::F32(x) => q.bind(x),
256            Value::F64(x) => q.bind(x),
257            Value::Text(x) => q.bind(x),
258            Value::Bytes(x) => q.bind(x),
259            Value::Array(items) => bind_array(q, items)?,
260            #[cfg(feature = "chrono")]
261            Value::Date(x) => q.bind(x),
262            #[cfg(feature = "chrono")]
263            Value::Time(x) => q.bind(x),
264            #[cfg(feature = "chrono")]
265            Value::DateTime(x) => q.bind(x),
266            #[cfg(feature = "chrono")]
267            Value::TimestampTz(x) => q.bind(x),
268            #[cfg(feature = "uuid")]
269            Value::Uuid(x) => q.bind(x),
270            #[cfg(feature = "decimal")]
271            Value::Decimal(x) => q.bind(x),
272            #[cfg(feature = "json")]
273            Value::Json(x) => q.bind(x),
274            other => return Err(unsupported_value(other.type_name())),
275        };
276    }
277    Ok(q)
278}
279
280fn unsupported_value(type_name: &'static str) -> ExecError {
281    ExecError::UnsupportedValue {
282        type_name,
283        family: Family::Postgres,
284    }
285}
286
287/// PostgreSQL arrays are typed, so the element variant picks the array type.
288/// Elements must be homogeneous; `Null` elements ride along as SQL `NULL`s.
289/// An empty array binds as `text[]` (there is nothing to infer from) — cast
290/// in SQL if the column is another array type.
291fn bind_array<'q>(
292    q: sqlx::query::Query<'q, Postgres, PgArguments>,
293    items: Vec<Value>,
294) -> Result<sqlx::query::Query<'q, Postgres, PgArguments>, ExecError> {
295    macro_rules! typed {
296        ($variant:ident, $t:ty) => {{
297            let mut out: Vec<Option<$t>> = Vec::with_capacity(items.len());
298            for item in items {
299                match item {
300                    Value::$variant(x) => out.push(Some(x)),
301                    Value::Null => out.push(None),
302                    other => return Err(unsupported_value(other.type_name())),
303                }
304            }
305            q.bind(out)
306        }};
307    }
308    let first = items.iter().find(|v| !v.is_null());
309    Ok(match first {
310        None => {
311            // All NULLs or empty: text[] is the only honest default.
312            let nulls: Vec<Option<String>> = items.iter().map(|_| None).collect();
313            q.bind(nulls)
314        }
315        Some(Value::Bool(_)) => typed!(Bool, bool),
316        Some(Value::I16(_)) => typed!(I16, i16),
317        Some(Value::I32(_)) => typed!(I32, i32),
318        Some(Value::I64(_)) => typed!(I64, i64),
319        Some(Value::F32(_)) => typed!(F32, f32),
320        Some(Value::F64(_)) => typed!(F64, f64),
321        Some(Value::Text(_)) => typed!(Text, String),
322        #[cfg(feature = "uuid")]
323        Some(Value::Uuid(_)) => typed!(Uuid, uuid::Uuid),
324        Some(other) => return Err(unsupported_value(other.type_name())),
325    })
326}
327
328/// Native row → keelson [`Row`], per the column-type column of the mappings
329/// table. The header is built once per result set and shared.
330fn decode_row(row: &PgRow, header: &mut Option<Arc<[Column]>>) -> Result<Row, ExecError> {
331    let columns = header
332        .get_or_insert_with(|| {
333            row.columns()
334                .iter()
335                .map(|c| Column::new(c.name()))
336                .collect::<Vec<_>>()
337                .into()
338        })
339        .clone();
340    let mut values = Vec::with_capacity(row.columns().len());
341    for i in 0..row.columns().len() {
342        values.push(decode_value(row, i)?);
343    }
344    Ok(Row::new(columns, values))
345}
346
347fn decode_value(row: &PgRow, i: usize) -> Result<Value, ExecError> {
348    let col = &row.columns()[i];
349    let name = col.name();
350    let raw = row.try_get_raw(i).map_err(|e| decode_err(name, e))?;
351    if raw.is_null() {
352        return Ok(Value::Null);
353    }
354    let ty = raw.type_info();
355    let ty = ty.name();
356
357    macro_rules! take {
358        ($t:ty) => {
359            row.try_get::<$t, _>(i).map_err(|e| decode_err(name, e))?
360        };
361    }
362
363    Ok(match ty {
364        "BOOL" => Value::Bool(take!(bool)),
365        "INT2" => Value::I16(take!(i16)),
366        "INT4" => Value::I32(take!(i32)),
367        "INT8" => Value::I64(take!(i64)),
368        "FLOAT4" => Value::F32(take!(f32)),
369        "FLOAT8" => Value::F64(take!(f64)),
370        "TEXT" | "VARCHAR" | "BPCHAR" | "NAME" => Value::Text(take!(String)),
371        "BYTEA" => Value::Bytes(take!(Vec<u8>)),
372        #[cfg(feature = "chrono")]
373        "DATE" => Value::Date(take!(chrono::NaiveDate)),
374        #[cfg(feature = "chrono")]
375        "TIME" => Value::Time(take!(chrono::NaiveTime)),
376        #[cfg(feature = "chrono")]
377        "TIMESTAMP" => Value::DateTime(take!(chrono::NaiveDateTime)),
378        #[cfg(feature = "chrono")]
379        "TIMESTAMPTZ" => Value::TimestampTz(take!(chrono::DateTime<chrono::Utc>)),
380        #[cfg(not(feature = "chrono"))]
381        "DATE" | "TIME" | "TIMESTAMP" | "TIMESTAMPTZ" => {
382            return Err(crate::common::need_feature(name, ty, "chrono"));
383        }
384        #[cfg(feature = "uuid")]
385        "UUID" => Value::Uuid(take!(uuid::Uuid)),
386        #[cfg(not(feature = "uuid"))]
387        "UUID" => return Err(crate::common::need_feature(name, ty, "uuid")),
388        #[cfg(feature = "decimal")]
389        "NUMERIC" => Value::Decimal(take!(rust_decimal::Decimal)),
390        #[cfg(not(feature = "decimal"))]
391        "NUMERIC" => return Err(crate::common::need_feature(name, ty, "decimal")),
392        #[cfg(feature = "json")]
393        "JSON" | "JSONB" => Value::Json(take!(serde_json::Value)),
394        #[cfg(not(feature = "json"))]
395        "JSON" | "JSONB" => return Err(crate::common::need_feature(name, ty, "json")),
396        "BOOL[]" => array(take!(Vec<Option<bool>>), Value::Bool),
397        "INT2[]" => array(take!(Vec<Option<i16>>), Value::I16),
398        "INT4[]" => array(take!(Vec<Option<i32>>), Value::I32),
399        "INT8[]" => array(take!(Vec<Option<i64>>), Value::I64),
400        "FLOAT4[]" => array(take!(Vec<Option<f32>>), Value::F32),
401        "FLOAT8[]" => array(take!(Vec<Option<f64>>), Value::F64),
402        "TEXT[]" | "VARCHAR[]" => array(take!(Vec<Option<String>>), Value::Text),
403        #[cfg(feature = "uuid")]
404        "UUID[]" => array(take!(Vec<Option<uuid::Uuid>>), Value::Uuid),
405        other => return Err(unhandled(name, other)),
406    })
407}
408
409fn array<T>(items: Vec<Option<T>>, wrap: fn(T) -> Value) -> Value {
410    Value::Array(
411        items
412            .into_iter()
413            .map(|x| x.map_or(Value::Null, wrap))
414            .collect(),
415    )
416}