Skip to main content

arminius_lib_db/
lib.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4//! # Arminius Database Layer
5//! The common interface for application code to execute
6//! DB queries.
7//!
8//! In essence, this crate contains three types:
9//!   - [`DefaultDb`]: executes queries outside of a transaction, can
10//!     [start](`DefaultDb::begin_transaction`) a transaction.
11//!   - [`TransactedDb`]: executes queries inside of a transaction, can
12//!     [commit](`TransactedDb::commit`) and [roll back](`TransactedDb::rollback`) a transaction.
13//!   - [`Database`]: a trait implemented by both [`DefaultDb`] and [`TransactedDb`].
14//!
15//! This crate does not provide any queries by itself. These are meant to be
16//! added by other crates via extension traits on [`Database`].
17//!
18//! ## Writing queries
19//! ### Conventions
20//! Queries are meant to be defined in their own crate. The name of such crate
21//! should adhere to the pattern `arminius-lib-db-<table>`.
22//!
23//! As a rule of thumb, one such crate should only be responsible for one
24//! specific table. This allows for fine-grained control over what components
25//! are allowed to execute which queries. However, there might be situations
26//! where we deviate from this and have more than one table managed by an
27//! extension crate, because they relate very close to each other (`room` and
28//! `event` being an example for such). However, we should not end up having
29//! multiple crates for one table.
30//!
31//! Such crates should not depend on other crates, with the following
32//! exceptions:
33//! - `arminius-lib-db`
34//! - `arminius-lib-model`
35//! - `sqlx`
36//!
37//! SELECT queries should return their results using one of the types in
38//! `arminius-lib-model`, so that data is represented in a use case-agnostic way.
39//! This allows queries to be reused in different situations.
40//!
41//! ### Registering queries via extension trait
42//! Queries should be added as a trait:
43//!
44//! ```ignore
45//! /// the trait that contains the queries
46//! // This annotation will suggest imports as _ (preferred for extension traits).
47//!  #[rust_analyzer::prefer_underscore_import]
48//! trait FooQueries : Database {
49//! 	// note that queries functions are not abstract
50//! 	async fn create_foo(&mut self, ...) -> Result<arminius_lib_model::FooId> {
51//! 		sqlx::query!("select ...").fetch_all(self.connection())
52//! 	}
53//!
54//! 	// more queries methods here....
55//! }
56//! ```
57//!
58//! Make queries available to Database trait
59//! via a blanket implementation:
60//!
61//! ```ignore
62//! impl<T: Database> ExampleQueries for T {}
63//! ```
64
65pub mod query;
66mod tests;
67
68// TODO: remove those when db layer is done
69use sqlx::{
70	Acquire as _, ConnectOptions as _, Connection as _, PgPool, Postgres, Transaction,
71	migrate::Migrator,
72	pool::PoolConnection,
73	postgres::{PgConnectOptions, PgListener, PgPoolOptions},
74};
75/// We re-export some SQLX types, so these can be used
76pub use sqlx::{PgConnection as RawPgConnection, PgTransaction as RawPgTransaction};
77use url::Url;
78
79/// Provides access to a connection.
80/// Should be called by extension traits but not from application code.
81///
82/// Usually we don't need to use this directly, so prefer underscore for imports.
83#[rust_analyzer::prefer_underscore_import]
84pub trait Database {
85	/// Acquires a new connection from the pool.
86	///
87	/// [`TransactedDb::commit`]
88	async fn connection(&mut self) -> Result<&mut RawPgConnection, DatabaseError>;
89
90	/// Starts a new transaction, acquiring a fresh connection from the pool
91	/// Once a transaction has been started, it is the callers'
92	/// responsibility to make changes permanent by committing it.
93	/// If the transaction is dropped without being committed, all changes
94	/// are rolled back.
95	async fn begin_transaction<'d>(&'d mut self) -> Result<TransactedDb<'d>, DatabaseError>;
96}
97
98/// Default implementation that works without a transaction
99pub struct DefaultDb {
100	/// db pool
101	pool: PgPool,
102	/// when starting a transaction,
103	acquired: Option<PoolConnection<Postgres>>,
104}
105
106impl DefaultDb {
107	/// Creates a new db instance.
108	#[must_use]
109	pub fn new(pool: PgPool) -> DefaultDb {
110		DefaultDb { pool, acquired: None }
111	}
112
113	/// Connects to a postgresql database, using environment variables.
114	/// If the `DATABASE_URL` variable is set, connection options are derived
115	/// from it. Otherwise, refer to [PgConnectOptions::new_without_pgpass()]
116	/// for the specific environment variables used for configuration.
117	/// If the `ARMINIUS_LAZY` environment variable is set to `1` or `true`,
118	/// a lazy connection pool is created, meaning that an actual connection
119	/// attempt is deferred until the first query is executed.
120	pub async fn from_env() -> Result<Self, DatabaseError> {
121		let connect_options = if let Ok(url) = std::env::var("DATABASE_URL") {
122			PgConnectOptions::from_url(
123				&Url::parse(&url).map_err(|err| sqlx::Error::Configuration(err.into()))?,
124			)?
125		} else {
126			// Fall back to the "standard" postgres variables (PGHOST, PGUSER,
127			// etc). If these are not set, some hardcoded defaults are used.
128			// See [`PgConnectOptions::new_without_pgpass()`] for details.
129			PgConnectOptions::new()
130		};
131		// FIXME(#1377): Normalize the content of the variable.
132		let lazy = std::env::var("ARMINIUS_LAZY")
133			.is_ok_and(|var| var == "1" || var.to_lowercase() == "true");
134
135		Self::connect(connect_options, PgPoolOptions::new(), lazy).await
136	}
137
138	/// Connect to a PostgreSQL database endpoint. Creates a connection pool
139	/// with the given [PgConnectOptions] and [PgPoolOptions].
140	pub async fn connect(
141		pg_connect_options: PgConnectOptions,
142		pg_pool_options: PgPoolOptions,
143		lazy: bool,
144	) -> Result<Self, DatabaseError> {
145		let pool = if lazy {
146			pg_pool_options.connect_lazy_with(pg_connect_options)
147		} else {
148			pg_pool_options.connect_with(pg_connect_options).await?
149		};
150		Ok(Self::new(pool))
151	}
152
153	/// Run the migrations defined in the `migrations` directory.
154	pub async fn run_migrations(&self) -> Result<(), sqlx::Error> {
155		sqlx::migrate!().run(&self.pool).await?;
156		Ok(())
157	}
158
159	/// Run the migrations from the given `Migrator`
160	pub async fn run_migrations_with(&self, migrator: &Migrator) -> Result<(), sqlx::Error> {
161		migrator.run(&self.pool).await?;
162		Ok(())
163	}
164
165	/// Create a `PgListener` using this instance's connection pool
166	pub async fn listener(&self) -> Result<PgListener, DatabaseError> {
167		Ok(PgListener::connect_with(&self.pool).await?)
168	}
169
170	/// Run a database connectivity check
171	pub async fn ping(&self) -> Result<(), DatabaseError> {
172		self.clone().connection().await?.ping().await?;
173		Ok(())
174	}
175}
176
177impl Database for DefaultDb {
178	async fn connection(&mut self) -> Result<&mut RawPgConnection, DatabaseError> {
179		Ok(match &mut self.acquired {
180			Some(acquired) => acquired,
181			acquired @ None => acquired.insert(self.pool.acquire().await?),
182		})
183	}
184
185	async fn begin_transaction<'c>(&'c mut self) -> Result<TransactedDb<'c>, DatabaseError> {
186		let transaction = self.pool.begin().await?;
187		Ok(TransactedDb { transaction })
188	}
189}
190
191impl Clone for DefaultDb {
192	fn clone(&self) -> Self {
193		Self { pool: self.pool.clone(), acquired: None }
194	}
195}
196
197/// for whatever reason...
198impl std::fmt::Debug for DefaultDb {
199	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200		f.debug_struct("DefaultDb").finish_non_exhaustive()
201	}
202}
203
204/// Exposes the inner connection pool.
205/// Unfortunately, this is still required because
206/// some tests are still using sqlx directly.
207#[cfg(feature = "expose_pool")]
208impl AsRef<PgPool> for DefaultDb {
209	fn as_ref(&self) -> &PgPool {
210		&self.pool
211	}
212}
213
214/// DB Queries inside a transaction
215#[derive(Debug)]
216pub struct TransactedDb<'c> {
217	/// what shall I say... the transaction.
218	transaction: Transaction<'c, Postgres>,
219}
220
221/// Operations only valid inside a transaction
222impl<'c> TransactedDb<'c> {
223	/// Attempts to commit the current transaction.
224	pub async fn commit(self) -> Result<(), DatabaseError> {
225		self.transaction.commit().await.map_err(DatabaseError::from)
226	}
227
228	/// Attempts to rollback the current transaction.
229	pub async fn rollback(self) -> sqlx::Result<()> {
230		self.transaction.rollback().await
231	}
232}
233
234impl<'c> Database for TransactedDb<'c> {
235	async fn connection(&mut self) -> Result<&mut RawPgConnection, DatabaseError> {
236		Ok(self.transaction.acquire().await?)
237	}
238
239	/// Starts a nested transaction
240	async fn begin_transaction<'d>(&'d mut self) -> Result<TransactedDb<'d>, DatabaseError> {
241		let transaction = self.transaction.begin().await?;
242		Ok(TransactedDb { transaction })
243	}
244}
245
246/// A generic Error type to be used for functions that perform DB operations.
247///
248/// Note regarding sqlx Errors: Most sqlx errors are of technical nature
249/// (like ConnectionPool exhausted, connection unexpectedly closed,
250/// timeouts). These are likely not handled specifically by calling
251/// code, and mostly just cause the operation to fail.
252///
253/// Some sqlx errors (such as ConstraintViolation) may actually be useful
254/// for the caller. For example, in a signup process, we may very well want to
255/// handle the case where an email address is already registered. In this case,
256/// the recommended pattern is to create a new error type containing the errors
257/// you want the caller to handle explicitly, and include a variant that
258/// holds the DatabaseError. This makes it very clear to the caller what
259/// application-level conditions may occur.
260///
261/// Example:
262/// ```rust
263/// #[derive(thiserror::Error, Debug)]
264/// enum SignupError {
265/// 	// This typically comes from a sqlx constraint violation, but we want
266/// 	// the caller to handle this explicitly.
267/// 	#[error("username already taken")]
268/// 	UsernameAlreadyTaken,
269///
270/// 	// All other "uninteresting" sqlx errors will be signaled via the DatabaseError.
271/// 	#[error(transparent)]
272/// 	DatabaseError(arminius_lib_db::DatabaseError),
273/// }
274/// ```
275///
276/// Trivial query functions (like simple SELECT queries) that do not have any
277/// non-technical error cases may of course return this type directly.
278#[derive(thiserror::Error, Debug)]
279pub enum DatabaseError {
280	/// An sqlx error.
281	#[error(transparent)]
282	SqlxError(sqlx::Error),
283
284	/// Unexpected content read from the database. E.g. a JSON field
285	/// failed to deserialize, a field did not conform to a specific format
286	/// (like an email address) etc
287	#[error("Invalid database content: {0}")]
288	InvalidData(String),
289}
290
291/// Allows to convenient convert to a `Result<T, DatabaseError::InvalidData>`.
292///
293/// Extension traits don't need to be used directly so prefer importing `as _`.
294#[rust_analyzer::prefer_underscore_import]
295pub trait ResultExt<T> {
296	/// Maps a [`Result<T, E>`] to [`Result<T, DatabaseError>`]
297	fn invalid_data(self, details: &str) -> Result<T, DatabaseError>;
298}
299
300/// Transforms the `Result<T, E>` into a [`Result<T, DatabaseError>`].
301impl<T, E: std::error::Error> ResultExt<T> for Result<T, E> {
302	fn invalid_data(self, details: &str) -> Result<T, DatabaseError> {
303		self.map_err(|err| {
304			let message = format!("{details}: {err}");
305			// Emitting the tracing event here ensures that the error gets
306			// emitted from inside the query span, giving the correct context
307			// for the error, most importantly the arguments to the query
308			tracing::error!("Error in SQL data: {message}");
309			DatabaseError::InvalidData(message)
310		})
311	}
312}
313
314impl From<sqlx::Error> for DatabaseError {
315	fn from(value: sqlx::Error) -> Self {
316		// Emitting the tracing event here ensures that the error gets emitted
317		// from inside the query span, giving the correct context for the
318		// error, most importantly the arguments to the query
319		tracing::error!("Error executing SQL query: {value:?}");
320		Self::SqlxError(value)
321	}
322}