Wrap db plumbing into a Db struct
Replace the free open/migrate functions with a Db struct that owns
its path and handles migration at construction time. Callers get a
connection via Db::connect() instead of storing a raw PathBuf and
calling db::open() directly. QuireCi.db_path()/open_db() are replaced
by QuireCi.db() returning the Db.
https://claude.ai/code/session_01YXVu67hVnQSAY8wzmfmRHw
diff --git a/quire-ci/src/db.rs b/quire-ci/src/db.rs
index fabfada..4bf8e9c 100644
--- a/quire-ci/src/db.rs
+++ b/quire-ci/src/db.rs
@@ -1,4 +1,4 @@
-use std::path::Path;
+use std::path::{Path, PathBuf};
use rusqlite::Connection;
use rusqlite_migration::{M, Migrations};
@@ -8,14 +8,40 @@ static MIGRATIONS: std::sync::LazyLock<Migrations<'static>> = std::sync::LazyLoc
});
#[derive(Debug, thiserror::Error)]
-pub enum MigrationError {
+pub enum Error {
#[error(transparent)]
Sqlite(#[from] rusqlite::Error),
#[error("migration error: {0}")]
Migration(#[from] rusqlite_migration::Error),
}
-pub fn open(path: &Path) -> Result<Connection, rusqlite::Error> {
+/// An opened, migrated SQLite database. Cheap to clone — holds only the path.
+#[derive(Clone, Debug)]
+pub struct Db {
+ path: PathBuf,
+}
+
+impl Db {
+ /// Open the database at `path`, run pending migrations, and return a `Db`.
+ pub fn open(path: PathBuf) -> Result<Self, Error> {
+ let mut conn = connect(&path)?;
+ MIGRATIONS.to_latest(&mut conn)?;
+ let db = Self { path };
+ tracing::debug!(path = %db.path().display(), "database opened");
+ Ok(db)
+ }
+
+ /// Open a new connection to the database.
+ pub fn connect(&self) -> Result<Connection, rusqlite::Error> {
+ connect(&self.path)
+ }
+
+ pub fn path(&self) -> &Path {
+ &self.path
+ }
+}
+
+fn connect(path: &Path) -> Result<Connection, rusqlite::Error> {
let conn = Connection::open(path)?;
conn.execute_batch(
"PRAGMA journal_mode = WAL;
@@ -25,13 +51,8 @@ pub fn open(path: &Path) -> Result<Connection, rusqlite::Error> {
Ok(conn)
}
-pub fn migrate(conn: &mut Connection) -> Result<(), MigrationError> {
- MIGRATIONS.to_latest(conn)?;
- Ok(())
-}
-
#[cfg(test)]
-pub fn open_in_memory() -> Result<Connection, MigrationError> {
+pub fn open_in_memory() -> Result<Connection, Error> {
let mut conn = Connection::open_in_memory()?;
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
MIGRATIONS.to_latest(&mut conn)?;
diff --git a/quire-ci/src/quire.rs b/quire-ci/src/quire.rs
index 80d090c..f2d24ab 100644
--- a/quire-ci/src/quire.rs
+++ b/quire-ci/src/quire.rs
@@ -1,11 +1,10 @@
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
use miette::{IntoDiagnostic, Result};
-use rusqlite::Connection;
use quire_core::fennel::Fennel;
-use crate::db;
+use crate::db::Db;
/// Parsed global configuration (`<base-dir>/config.fnl`).
#[derive(serde::Deserialize, Debug, Clone)]
@@ -37,7 +36,7 @@ pub use quire_core::telemetry::SentryConfig;
#[derive(Clone)]
pub struct QuireCi {
config: GlobalConfig,
- db_path: PathBuf,
+ db: Db,
}
impl QuireCi {
@@ -49,22 +48,16 @@ impl QuireCi {
} else {
GlobalConfig::default()
};
- let db_path = base_dir.join("quire-ci.db");
- let mut conn = db::open(&db_path).into_diagnostic()?;
- db::migrate(&mut conn).into_diagnostic()?;
- Ok(Self { config, db_path })
+ let db = Db::open(base_dir.join("quire-ci.db")).into_diagnostic()?;
+ Ok(Self { config, db })
}
pub fn config(&self) -> &GlobalConfig {
&self.config
}
- pub fn db_path(&self) -> &Path {
- &self.db_path
- }
-
- pub fn open_db(&self) -> Result<Connection, rusqlite::Error> {
- db::open(self.db_path())
+ pub fn db(&self) -> &Db {
+ &self.db
}
}
@@ -135,7 +128,7 @@ mod tests {
fn db_is_created_at_expected_path() {
let dir = tempfile::tempdir().expect("tempdir");
let q = QuireCi::new(dir.path().to_path_buf()).expect("should load");
- assert_eq!(q.db_path(), dir.path().join("quire-ci.db"));
- assert!(q.db_path().exists(), "db file should exist after new()");
+ assert_eq!(q.db().path(), dir.path().join("quire-ci.db"));
+ assert!(q.db().path().exists(), "db file should exist after new()");
}
}
diff --git a/quire-ci/src/server.rs b/quire-ci/src/server.rs
index ded931d..18022f4 100644
--- a/quire-ci/src/server.rs
+++ b/quire-ci/src/server.rs
@@ -64,7 +64,7 @@ async fn webhook(
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
- let conn = match quire.open_db() {
+ let conn = match quire.db().connect() {
Ok(c) => c,
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR,
};
@@ -193,7 +193,7 @@ mod tests {
async fn valid_hmac_creates_run_row() {
let secret = "test-secret-key";
let (_dir, quire) = quire_with_secret(secret);
- let db_path = quire.db_path().to_path_buf();
+ let db = quire.db().clone();
let app = make_app(quire);
let body = push_event_body();
@@ -210,7 +210,7 @@ mod tests {
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
- let conn = crate::db::open(&db_path).expect("open db");
+ let conn = db.connect().expect("connect");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0))
.expect("count");
@@ -220,7 +220,7 @@ mod tests {
#[tokio::test]
async fn no_secret_configured_allows_unsigned_post() {
let (_dir, quire) = quire_without_secret();
- let db_path = quire.db_path().to_path_buf();
+ let db = quire.db().clone();
let app = make_app(quire);
let body = push_event_body();
@@ -235,7 +235,7 @@ mod tests {
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
- let conn = crate::db::open(&db_path).expect("open db");
+ let conn = db.connect().expect("connect");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0))
.expect("count");