1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-09 10:01:45 +03:00
Files
authentication-service/docs/development/database.md
2022-02-07 16:10:44 +01:00

3.3 KiB

Database

Interactions with the database goes through sqlx. It provides async database operations with connection pooling, migrations support and compile-time check of queries through macros.

Compile-time check of queries

To be able to check queries, sqlx has to introspect the live database. Usually it does so by having the database available at compile time, but to avoid that we're using the offline feature of sqlx, which saves the introspection informatons as a flat file in the repository.

Preparing this flat file is done through sqlx-cli, and should be done everytime the database schema or the queries changed.

# Install the CLI
cargo install sqlx-cli --no-default-features --features postgres

cd crates/storage/ # Must be in the mas-storage crate folder
export DATABASE_URL=postgresql:///matrix_auth
cargo sqlx prepare

Migrations

Migration files live in the migrations folder in the mas-core crate.

cd crates/storage/ # Again, in the mas-storage crate folder
export DATABASE_URL=postgresql:///matrix_auth
cargo sqlx migrate run # Run pending migrations
cargo sqlx migrate revert # Revert the last migration
cargo sqlx migrate add -r [description] # Add new migration files

Note that migrations are embedded in the final binary and can be run from the service CLI tool.

Writing database interactions

A typical interaction with the database look like this:

pub async fn lookup_session(
    executor: impl Executor<'_, Database = Postgres>,
    id: i64,
) -> anyhow::Result<SessionInfo> {
    sqlx::query_as!(
        SessionInfo, // Struct that will be filled with the result
        r#"
            SELECT
                s.id,
                u.id as user_id,
                u.username,
                s.active,
                s.created_at,
                a.created_at as "last_authd_at?"
            FROM user_sessions s
            INNER JOIN users u 
                ON s.user_id = u.id
            LEFT JOIN user_session_authentications a
                ON a.session_id = s.id
            WHERE s.id = $1
            ORDER BY a.created_at DESC
            LIMIT 1
        "#,
        id, // Query parameter
    )
    .fetch_one(executor)
    .await
    // Providing some context when there is an error
    .context("could not fetch session")
}

Note that we pass an impl Executor as parameter here. This allows us to use this function from either a simple connection or from an active transaction.

The caveat here is that the executor can be used only once, so if an interaction needs to do multiple queries, it should probably take an impl Acquire to then acquire a transaction and do multiple interactions.

pub async fn login(
    conn: impl Acquire<'_, Database = Postgres>,
    username: &str,
    password: String,
) -> Result<SessionInfo, LoginError> {
    let mut txn = conn.begin().await.context("could not start transaction")?;
    // First interaction
    let user = lookup_user_by_username(&mut txn, username)?;
    // Second interaction
    let mut session = start_session(&mut txn, user).await?;
    // Third interaction
    session.last_authd_at = 
        Some(authenticate_session(&mut txn, session.id, password).await?);
    // Commit the transaction once everything went fine
    txn.commit().await.context("could not commit transaction")?;
    Ok(session)
}