Files
mnemosyne/src/database/mod.rs

69 lines
2.1 KiB
Rust

use std::{env, error::Error, sync::LazyLock};
use axum::{http::StatusCode, response::IntoResponse};
use rusqlite::{Connection, OptionalExtension};
macro_rules! migration {
($name:literal) => {
($name, include_str!(concat!("./migrations/", $name, ".sql")))
};
}
const MIGRATIONS: &[(&str, &str)] = &[migration!("2026-03-07--01")];
pub static DB_URL: LazyLock<String> =
LazyLock::new(|| env::var("DATABASE_URL").expect("DATABASE_URL is not set"));
const PERSISTENT_PRAGMAS: &[&str] = &["PRAGMA journal_mode = WAL"];
const CONNECTION_PRAGMAS: &[&str] = &["PRAGMA foreign_keys = ON", "PRAGMA busy_timeout = 5000"];
const TABLE_MIGRATIONS: &str = r#"
CREATE TABLE IF NOT EXISTS migrations (
id TEXT PRIMARY KEY,
time INTEGER DEFAULT (unixepoch())
);
"#;
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct DatabaseError(#[from] rusqlite::Error);
impl IntoResponse for DatabaseError {
fn into_response(self) -> axum::response::Response {
log::error!("[DB ERROR] {}", self);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error.").into_response()
}
}
pub fn conn() -> Result<Connection, DatabaseError> {
let conn = Connection::open(&*DB_URL)?;
for pragma in CONNECTION_PRAGMAS {
conn.query_row(pragma, (), |_| Ok(())).optional()?;
}
Ok(conn)
}
pub fn migrations() -> Result<(), Box<dyn Error>> {
let conn = Connection::open(&*DB_URL)?;
for pragma in PERSISTENT_PRAGMAS {
conn.query_row(pragma, (), |_| Ok(()))?;
}
conn.execute(TABLE_MIGRATIONS, ())?;
let mut changes = false;
for (key, sql) in MIGRATIONS {
let mut statement = conn.prepare("SELECT id, time FROM migrations WHERE id = ?1")?;
let query = statement.query_one([key], |_| Ok(())).optional()?;
if query.is_some() {
continue;
}
changes = true;
log::info!("Applying migration {key}...");
conn.execute_batch(sql)?;
conn.execute("INSERT INTO migrations(id) VALUES (?1)", [key])?;
}
if changes {
log::info!("Migrations applied.")
}
Ok(())
}