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};