ag_store/error.rs
1//! Database error types shared by persistence adapters.
2
3/// Typed error returned by database operations.
4///
5/// Wraps the underlying `SQLx`, migration, and I/O failures so callers can
6/// distinguish error categories without parsing opaque strings.
7#[derive(Debug, thiserror::Error)]
8pub enum DbError {
9 /// A SQL query or connection-pool operation failed.
10 #[error("{0}")]
11 Query(#[from] sqlx::Error),
12
13 /// An embedded schema migration failed during database open.
14 #[error("{0}")]
15 Migration(#[from] sqlx::migrate::MigrateError),
16
17 /// A filesystem operation failed, such as creating the database directory.
18 #[error("{0}")]
19 Io(#[from] std::io::Error),
20
21 /// Caller-provided or persisted data violated a domain invariant.
22 #[error("Invalid {entity}: {reason}")]
23 InvalidData {
24 /// Persistence entity whose data failed validation.
25 entity: &'static str,
26 /// Human-readable invariant violation.
27 reason: String,
28 },
29
30 /// A persisted lifecycle value could not be decoded by its owning adapter.
31 #[error("Invalid {entity} lifecycle status `{value}`")]
32 InvalidStatus {
33 /// Persistence entity whose status failed validation.
34 entity: &'static str,
35 /// Unrecognized stored or caller-provided value.
36 value: String,
37 },
38
39 /// A query failed during a named persistence operation.
40 #[error("Database operation `{operation}` failed: {source}")]
41 QueryContext {
42 /// Stable semantic label for the failed operation.
43 operation: &'static str,
44 /// Underlying `SQLx` failure.
45 #[source]
46 source: sqlx::Error,
47 },
48}
49
50/// Adds a semantic persistence-operation label to a query result.
51pub(crate) trait DbResultExt<T> {
52 /// Maps a raw `SQLx` error into [`DbError::QueryContext`].
53 fn db_context(self, operation: &'static str) -> Result<T, DbError>;
54}
55
56impl<T> DbResultExt<T> for Result<T, sqlx::Error> {
57 fn db_context(self, operation: &'static str) -> Result<T, DbError> {
58 self.map_err(|source| DbError::QueryContext { operation, source })
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn query_context_preserves_operation_and_source() {
68 // Arrange
69 let result = Err::<(), _>(sqlx::Error::RowNotFound);
70
71 // Act
72 let error = result
73 .db_context("load session")
74 .expect_err("query should fail");
75
76 // Assert
77 assert!(matches!(
78 error,
79 DbError::QueryContext {
80 operation: "load session",
81 source: sqlx::Error::RowNotFound,
82 }
83 ));
84 assert_eq!(
85 error.to_string(),
86 "Database operation `load session` failed: no rows returned by a query that expected \
87 to return at least one row"
88 );
89 }
90}