mas_storage_pg/lib.rs
1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! An implementation of the storage traits for a PostgreSQL database
8//!
9//! This backend uses [`sqlx`] to interact with the database. Most queries are
10//! type-checked, using introspection data recorded in the `sqlx-data.json`
11//! file. This file is generated by the `sqlx` CLI tool, and should be updated
12//! whenever the database schema changes, or new queries are added.
13//!
14//! # Implementing a new repository
15//!
16//! When a new repository is defined in [`mas_storage`], it should be
17//! implemented here, with the PostgreSQL backend.
18//!
19//! A typical implementation will look like this:
20//!
21//! ```rust
22//! # use async_trait::async_trait;
23//! # use ulid::Ulid;
24//! # use rand::RngCore;
25//! # use mas_storage::Clock;
26//! # use mas_storage_pg::{DatabaseError, ExecuteExt};
27//! # use sqlx::PgConnection;
28//! # use uuid::Uuid;
29//! #
30//! # // A fake data structure, usually defined in mas-data-model
31//! # #[derive(sqlx::FromRow)]
32//! # struct FakeData {
33//! # id: Ulid,
34//! # }
35//! #
36//! # // A fake repository trait, usually defined in mas-storage
37//! # #[async_trait]
38//! # pub trait FakeDataRepository: Send + Sync {
39//! # type Error;
40//! # async fn lookup(&mut self, id: Ulid) -> Result<Option<FakeData>, Self::Error>;
41//! # async fn add(
42//! # &mut self,
43//! # rng: &mut (dyn RngCore + Send),
44//! # clock: &dyn Clock,
45//! # ) -> Result<FakeData, Self::Error>;
46//! # }
47//! #
48//! /// An implementation of [`FakeDataRepository`] for a PostgreSQL connection
49//! pub struct PgFakeDataRepository<'c> {
50//! conn: &'c mut PgConnection,
51//! }
52//!
53//! impl<'c> PgFakeDataRepository<'c> {
54//! /// Create a new [`FakeDataRepository`] from an active PostgreSQL connection
55//! pub fn new(conn: &'c mut PgConnection) -> Self {
56//! Self { conn }
57//! }
58//! }
59//!
60//! #[derive(sqlx::FromRow)]
61//! struct FakeDataLookup {
62//! fake_data_id: Uuid,
63//! }
64//!
65//! impl From<FakeDataLookup> for FakeData {
66//! fn from(value: FakeDataLookup) -> Self {
67//! Self {
68//! id: value.fake_data_id.into(),
69//! }
70//! }
71//! }
72//!
73//! #[async_trait]
74//! impl<'c> FakeDataRepository for PgFakeDataRepository<'c> {
75//! type Error = DatabaseError;
76//!
77//! #[tracing::instrument(
78//! name = "db.fake_data.lookup",
79//! skip_all,
80//! fields(
81//! db.query.text,
82//! fake_data.id = %id,
83//! ),
84//! err,
85//! )]
86//! async fn lookup(&mut self, id: Ulid) -> Result<Option<FakeData>, Self::Error> {
87//! // Note: here we would use the macro version instead, but it's not possible here in
88//! // this documentation example
89//! let res: Option<FakeDataLookup> = sqlx::query_as(
90//! r#"
91//! SELECT fake_data_id
92//! FROM fake_data
93//! WHERE fake_data_id = $1
94//! "#,
95//! )
96//! .bind(Uuid::from(id))
97//! .traced()
98//! .fetch_optional(&mut *self.conn)
99//! .await?;
100//!
101//! let Some(res) = res else { return Ok(None) };
102//!
103//! Ok(Some(res.into()))
104//! }
105//!
106//! #[tracing::instrument(
107//! name = "db.fake_data.add",
108//! skip_all,
109//! fields(
110//! db.query.text,
111//! fake_data.id,
112//! ),
113//! err,
114//! )]
115//! async fn add(
116//! &mut self,
117//! rng: &mut (dyn RngCore + Send),
118//! clock: &dyn Clock,
119//! ) -> Result<FakeData, Self::Error> {
120//! let created_at = clock.now();
121//! let id = Ulid::from_datetime_with_source(created_at.into(), rng);
122//! tracing::Span::current().record("fake_data.id", tracing::field::display(id));
123//!
124//! // Note: here we would use the macro version instead, but it's not possible here in
125//! // this documentation example
126//! sqlx::query(
127//! r#"
128//! INSERT INTO fake_data (id)
129//! VALUES ($1)
130//! "#,
131//! )
132//! .bind(Uuid::from(id))
133//! .traced()
134//! .execute(&mut *self.conn)
135//! .await?;
136//!
137//! Ok(FakeData {
138//! id,
139//! })
140//! }
141//! }
142//! ```
143//!
144//! A few things to note with the implementation:
145//!
146//! - All methods are traced, with an explicit, somewhat consistent name.
147//! - The SQL statement is included as attribute, by declaring a
148//! `db.query.text` attribute on the tracing span, and then calling
149//! [`ExecuteExt::traced`].
150//! - The IDs are all [`Ulid`], and generated from the clock and the random
151//! number generated passed as parameters. The generated IDs are recorded in
152//! the span.
153//! - The IDs are stored as [`Uuid`] in PostgreSQL, so conversions are required
154//! - "Not found" errors are handled by returning `Ok(None)` instead of an
155//! error.
156//!
157//! [`Ulid`]: ulid::Ulid
158//! [`Uuid`]: uuid::Uuid
159
160#![deny(clippy::future_not_send, missing_docs)]
161#![allow(clippy::module_name_repetitions, clippy::blocks_in_conditions)]
162
163use sqlx::migrate::Migrator;
164
165pub mod app_session;
166pub mod compat;
167pub mod oauth2;
168pub mod queue;
169pub mod upstream_oauth2;
170pub mod user;
171
172mod errors;
173pub(crate) mod filter;
174pub(crate) mod iden;
175pub(crate) mod pagination;
176pub(crate) mod policy_data;
177pub(crate) mod repository;
178pub(crate) mod tracing;
179
180pub(crate) use self::errors::DatabaseInconsistencyError;
181pub use self::{errors::DatabaseError, repository::PgRepository, tracing::ExecuteExt};
182
183/// Embedded migrations, allowing them to run on startup
184pub static MIGRATOR: Migrator = {
185 // XXX: The macro does not let us ignore missing migrations, so we have to do it
186 // like this. See https://github.com/launchbadge/sqlx/issues/1788
187 let mut m = sqlx::migrate!();
188
189 // We manually removed some migrations because they made us depend on the
190 // `pgcrypto` extension. See: https://github.com/matrix-org/matrix-authentication-service/issues/1557
191 m.ignore_missing = true;
192 m
193};