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}