Use crate's typed Error in web::db
Mapping rusqlite::Error to String dropped its source chain, leaving
both tracing and the error template with just the top-level message.
Switch the loaders to crate::Result and let `?` propagate via the
existing Error::Sql variant; render details through display_chain so
the full chain reaches logs and the error page.
Assisted-by: Claude Opus 4.7 via Claude Code
diff --git a/src/quire/web/db.rs b/src/quire/web/db.rs
index 6a4c052..47a24bb 100644
--- a/src/quire/web/db.rs
+++ b/src/quire/web/db.rs
@@ -2,7 +2,7 @@
use rusqlite::Connection;
-use crate::Quire;
+use crate::{Quire, Result};
/// Raw run row from the database.
pub struct RunRow {
@@ -33,16 +33,14 @@ pub struct ShEvent {
pub cmd: String,
}
-pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>, String> {
- let db = Connection::open(quire.db_path()).map_err(|e| e.to_string())?;
- let mut stmt = db
- .prepare(
- "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
- FROM runs WHERE repo = ?1
- ORDER BY queued_at_ms DESC
- LIMIT 50",
- )
- .map_err(|e| e.to_string())?;
+pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>> {
+ let db = Connection::open(quire.db_path())?;
+ let mut stmt = db.prepare(
+ "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
+ FROM runs WHERE repo = ?1
+ ORDER BY queued_at_ms DESC
+ LIMIT 50",
+ )?;
let rows = stmt
.query_map(rusqlite::params![repo], |row| {
@@ -55,10 +53,8 @@ pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>, String> {
started_at_ms: row.get(5)?,
finished_at_ms: row.get(6)?,
})
- })
- .map_err(|e| e.to_string())?
- .collect::<Result<Vec<_>, _>>()
- .map_err(|e| e.to_string())?;
+ })?
+ .collect::<rusqlite::Result<Vec<_>>>()?;
Ok(rows)
}
@@ -67,35 +63,31 @@ pub fn load_run_detail(
quire: &Quire,
repo: &str,
run_id: &str,
-) -> Result<(RunRow, Vec<JobRow>, Vec<ShEvent>), String> {
- let db = Connection::open(quire.db_path()).map_err(|e| e.to_string())?;
-
- let run = db
- .query_row(
- "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
- FROM runs WHERE id = ?1 AND repo = ?2",
- rusqlite::params![run_id, repo],
- |row| {
- Ok(RunRow {
- id: row.get(0)?,
- state: row.get(1)?,
- sha: row.get(2)?,
- ref_name: row.get(3)?,
- queued_at_ms: row.get(4)?,
- started_at_ms: row.get(5)?,
- finished_at_ms: row.get(6)?,
- })
- },
- )
- .map_err(|e| e.to_string())?;
-
- let mut job_stmt = db
- .prepare(
- "SELECT job_id, state, exit_code, started_at_ms, finished_at_ms
- FROM jobs WHERE run_id = ?1
- ORDER BY rowid",
- )
- .map_err(|e| e.to_string())?;
+) -> Result<(RunRow, Vec<JobRow>, Vec<ShEvent>)> {
+ let db = Connection::open(quire.db_path())?;
+
+ let run = db.query_row(
+ "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
+ FROM runs WHERE id = ?1 AND repo = ?2",
+ rusqlite::params![run_id, repo],
+ |row| {
+ Ok(RunRow {
+ id: row.get(0)?,
+ state: row.get(1)?,
+ sha: row.get(2)?,
+ ref_name: row.get(3)?,
+ queued_at_ms: row.get(4)?,
+ started_at_ms: row.get(5)?,
+ finished_at_ms: row.get(6)?,
+ })
+ },
+ )?;
+
+ let mut job_stmt = db.prepare(
+ "SELECT job_id, state, exit_code, started_at_ms, finished_at_ms
+ FROM jobs WHERE run_id = ?1
+ ORDER BY rowid",
+ )?;
let jobs = job_stmt
.query_map(rusqlite::params![run_id], |row| {
@@ -106,18 +98,14 @@ pub fn load_run_detail(
started_at_ms: row.get(3)?,
finished_at_ms: row.get(4)?,
})
- })
- .map_err(|e| e.to_string())?
- .collect::<Result<Vec<_>, _>>()
- .map_err(|e| e.to_string())?;
-
- let mut sh_stmt = db
- .prepare(
- "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
- FROM sh_events WHERE run_id = ?1
- ORDER BY job_id, started_at_ms",
- )
- .map_err(|e| e.to_string())?;
+ })?
+ .collect::<rusqlite::Result<Vec<_>>>()?;
+
+ let mut sh_stmt = db.prepare(
+ "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
+ FROM sh_events WHERE run_id = ?1
+ ORDER BY job_id, started_at_ms",
+ )?;
let sh_events = sh_stmt
.query_map(rusqlite::params![run_id], |row| {
@@ -128,10 +116,8 @@ pub fn load_run_detail(
exit_code: row.get(3)?,
cmd: row.get(4)?,
})
- })
- .map_err(|e| e.to_string())?
- .collect::<Result<Vec<_>, _>>()
- .map_err(|e| e.to_string())?;
+ })?
+ .collect::<rusqlite::Result<Vec<_>>>()?;
Ok((run, jobs, sh_events))
}
diff --git a/src/quire/web/handlers.rs b/src/quire/web/handlers.rs
index cbd2c15..55d1c00 100644
--- a/src/quire/web/handlers.rs
+++ b/src/quire/web/handlers.rs
@@ -8,6 +8,7 @@ use axum::response::{Html, IntoResponse, Response};
use super::db;
use super::templates::*;
use crate::Quire;
+use crate::error::display_chain;
/// Render a template into an HTML response, returning 500 on render failure.
fn render<T: Template>(tmpl: &T) -> Response {
@@ -48,12 +49,12 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
let runs = match db::load_runs(&quire, &repo_name) {
Ok(r) => r,
Err(e) => {
- tracing::error!(repo = %repo, error = %e, "failed to load runs");
+ tracing::error!(repo = %repo, error = %display_chain(&e), "failed to load runs");
return render_error(
repo_display,
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load runs",
- e,
+ display_chain(&e).to_string(),
);
}
};
@@ -93,12 +94,12 @@ pub async fn run_detail(
let (run, jobs, sh_events) = match result {
Ok(d) => d,
Err(e) => {
- tracing::error!(repo = %repo, run_id = %run_id, error = %e, "failed to load run detail");
+ tracing::error!(repo = %repo, run_id = %run_id, error = %display_chain(&e), "failed to load run detail");
return render_error(
repo_display,
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load run",
- e,
+ display_chain(&e).to_string(),
);
}
};