Skip to main content

axum_postgres_tx/
lib.rs

1#![deny(missing_docs, clippy::unwrap_used)]
2#![warn(clippy::impl_trait_in_params)]
3
4//! Request-scoped PostgreSQL transactions for axum.
5//!
6//! This crate provides a Tower [`Layer`](layer::Layer) and [`Service`](layer::Service)
7//! that automatically manage request-scoped PostgreSQL transactions using
8//! either `bb8-postgres` or `deadpool-postgres`. Handlers can extract a
9//! [`Tx`](tx::Tx) to access the transaction; it is committed automatically
10//! when the response is sent.
11//!
12//! # Examples
13//!
14//! ```rust,no_run
15//! use axum::extract::State;
16//! use axum_postgres_tx::Tx;
17//!
18//! async fn handler(State(pool): State<axum_postgres_tx::pool::Pool>, Tx(tx): Tx) -> String {
19//!     let row = tx.query_one("SELECT 1", &[]).await.unwrap();
20//!     format!("{}", row.get::<_, i32>(0))
21//! }
22//! ```
23
24use axum_core::response::{IntoResponse, Response};
25use http::StatusCode;
26use thiserror::Error;
27
28#[cfg(feature = "bb8")]
29use bb8_postgres::{
30    PostgresConnectionManager,
31    bb8::{PooledConnection, RunError},
32    tokio_postgres::{self, NoTls, Transaction},
33};
34
35#[cfg(feature = "deadpool")]
36use deadpool_postgres::tokio_postgres::{self, Transaction};
37
38mod extension;
39/// Tower [`Layer`](tower_layer::Layer) and [`Service`](tower_service::Service)
40/// for managing request-scoped PostgreSQL transactions.
41pub mod layer;
42/// Re-exported pool types for use in application state.
43pub mod pool;
44/// The [`Tx`](tx::Tx) extractor for accessing the current transaction.
45pub mod tx;
46
47#[cfg(feature = "bb8")]
48type Pool = bb8_postgres::bb8::Pool<PostgresConnectionManager<NoTls>>;
49#[cfg(feature = "deadpool")]
50type Pool = deadpool_postgres::Pool;
51
52/// Errors that can occur when using this crate.
53#[derive(Debug, Error)]
54pub enum Error {
55    /// Failed to obtain a connection from the pool.
56    #[cfg(feature = "bb8")]
57    #[error(transparent)]
58    Pool(#[from] RunError<tokio_postgres::Error>),
59    /// Failed to obtain a connection from the pool.
60    #[cfg(feature = "deadpool")]
61    #[error(transparent)]
62    Pool(#[from] deadpool::managed::PoolError<deadpool_postgres::tokio_postgres::Error>),
63    /// A PostgreSQL error occurred while executing a query or transaction.
64    #[error(transparent)]
65    Db(#[from] tokio_postgres::Error),
66    /// The [`Layer`](layer::Layer) was not applied to the router.
67    ///
68    /// This occurs when [`Tx`](tx::Tx) is extracted in a handler but the
69    /// request was not processed by the transaction [`Layer`](layer::Layer).
70    #[error("required extension not registered")]
71    MissingExtension,
72    /// [`Tx`](tx::Tx) was extracted multiple times in the same handler or middleware.
73    ///
74    /// Only one `Tx` extractor can be used per request. If you need to run
75    /// multiple queries, use the same transaction for all of them.
76    #[error("Tx extractor used multiple times in the same handler/middleware")]
77    OverlappingExtractors,
78}
79
80impl IntoResponse for Error {
81    fn into_response(self) -> Response {
82        (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
83    }
84}
85
86/// A transaction paired with the connection it was started on.
87///
88/// This is a self-referential struct: `tx` borrows from `_conn`. The
89/// `transmute` in [`OwnedTx::new`] extends the transaction's lifetime to
90/// `'static` so both fields can be stored together. This is sound because:
91///
92/// 1. `_conn` is [`Box`]-ed, giving it a stable address that never moves.
93/// 2. `tx` is declared **before** `_conn`, so Rust's drop order guarantees
94///    the transaction is dropped while the connection is still alive.
95/// 3. [`tokio_postgres::Transaction<'a>`] is covariant over `'a` (it holds
96///    `&'a mut Client`), so a `Transaction<'short>` can be soundly
97///    reinterpreted as `Transaction<'long>` when `'short: 'long`.
98struct OwnedTx {
99    // SAFETY INVARIANT: `tx` must be declared before `_conn` so that `tx` is
100    // dropped while `_conn` is still alive.
101    tx: Transaction<'static>,
102    #[cfg(feature = "bb8")]
103    _conn: Box<PooledConnection<'static, PostgresConnectionManager<NoTls>>>,
104    #[cfg(feature = "deadpool")]
105    _conn: Box<deadpool_postgres::Object>,
106}
107
108impl OwnedTx {
109    /// Start a transaction on the given connection.
110    ///
111    /// The connection is boxed to ensure a stable address for the
112    /// self-referential borrow. See the [`OwnedTx`] struct docs for the
113    /// safety argument.
114    #[cfg(feature = "bb8")]
115    async fn new(
116        conn: PooledConnection<'static, PostgresConnectionManager<NoTls>>,
117        read_only: bool,
118    ) -> Result<OwnedTx, tokio_postgres::Error> {
119        let mut conn = Box::new(conn);
120        let tx = conn
121            .build_transaction()
122            .read_only(read_only)
123            .start()
124            .await?;
125        // SAFETY: `conn` is boxed (stable address). `tx` borrows `*conn` but
126        // we need to store both in the same struct. This is sound because:
127        //   1. Box guarantees the connection's address never changes.
128        //   2. Field declaration order ensures `tx` drops before `_conn`.
129        //   3. Transaction<'a> is covariant over 'a, so
130        //      Transaction<'_> (where '_ is the borrow of *conn) can be
131        //      reinterpreted as Transaction<'static> — the connection
132        //      remains alive for the struct's entire lifetime.
133        // The transaction MUST be dropped before the connection, which is
134        // guaranteed by the field declaration order above.
135        let tx = unsafe {
136            std::mem::transmute::<
137                tokio_postgres::Transaction<'_>,
138                tokio_postgres::Transaction<'static>,
139            >(tx)
140        };
141        Ok(OwnedTx { tx, _conn: conn })
142    }
143
144    /// Start a transaction on the given connection.
145    ///
146    /// The connection is boxed to ensure a stable address for the
147    /// self-referential borrow. See the [`OwnedTx`] struct docs for the
148    /// safety argument.
149    #[cfg(feature = "deadpool")]
150    async fn new(
151        conn: deadpool_postgres::Object,
152        read_only: bool,
153    ) -> Result<OwnedTx, tokio_postgres::Error> {
154        use std::ops::DerefMut;
155
156        let mut conn = Box::new(conn);
157        // Bypass deadpool's wrapper to get the raw tokio_postgres::Client.
158        // Box<Object> -> Object (DerefMut) -> ClientWrapper (DerefMut) -> tokio_postgres::Client
159        let client: &mut tokio_postgres::Client = conn.deref_mut().deref_mut().deref_mut();
160        let tx = client
161            .build_transaction()
162            .read_only(read_only)
163            .start()
164            .await?;
165        let tx = unsafe {
166            std::mem::transmute::<
167                tokio_postgres::Transaction<'_>,
168                tokio_postgres::Transaction<'static>,
169            >(tx)
170        };
171        Ok(OwnedTx { tx, _conn: conn })
172    }
173}