Skip to main content

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}