Skip to main content

systemprompt_database/
error.rs

1//! Typed error boundary for the database crate.
2//!
3//! `RepositoryError` is the canonical error returned from the crate's
4//! database-facing public signatures, including the dyn-safe
5//! `DatabaseProvider` / `DatabaseTransaction` trait surfaces. It composes
6//! `sqlx::Error` and `serde_json::Error` via `#[from]`; runtime invariant
7//! failures are routed through `RepositoryError::InvalidState`.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum RepositoryError {
16    #[error("Entity not found: {0}")]
17    NotFound(String),
18
19    #[error("Constraint violation: {0}")]
20    Constraint(String),
21
22    #[error("Database error: {0}")]
23    Database(#[from] sqlx::Error),
24
25    #[error("Serialization error: {0}")]
26    Serialization(#[from] serde_json::Error),
27
28    #[error("Invalid argument: {0}")]
29    InvalidArgument(String),
30
31    #[error("Invalid state: {0}")]
32    InvalidState(String),
33
34    #[error("Internal error: {0}")]
35    Internal(String),
36
37    #[error("Failed to execute query")]
38    QueryExecution(#[source] Box<Self>),
39
40    #[error("SQL could not be split into statements: {0}")]
41    SqlSplit(#[source] pg_query::Error),
42
43    #[error("Failed to execute SQL statement: {statement}")]
44    Statement {
45        statement: String,
46        #[source]
47        source: Box<Self>,
48    },
49
50    #[error("Failed to establish database connection")]
51    Connection(#[source] Box<Self>),
52
53    #[error("Failed to read SQL file {path}")]
54    SqlFile {
55        path: String,
56        #[source]
57        source: std::io::Error,
58    },
59}
60
61pub type DatabaseResult<T> = Result<T, RepositoryError>;
62
63impl RepositoryError {
64    pub fn not_found<T: std::fmt::Display>(id: T) -> Self {
65        Self::NotFound(id.to_string())
66    }
67
68    pub fn is_serialization_failure(&self) -> bool {
69        // Why: Postgres aborts one side of a serialization conflict (40001) or a
70        // deadlock (40P01) and documents both as "retry the transaction".
71        match self {
72            Self::Database(sqlx_error) => sqlx_error.as_database_error().is_some_and(|db_error| {
73                let code = db_error.code().map(|c| c.to_string());
74                matches!(code.as_deref(), Some("40001" | "40P01"))
75            }),
76            _ => false,
77        }
78    }
79
80    // Why: Postgres refuses CREATE OR REPLACE FUNCTION that changes the return
81    // type or parameter names of an existing function (42P13); only DROP then
82    // CREATE reshapes it.
83    pub fn is_invalid_function_definition(&self) -> bool {
84        match self {
85            Self::Database(sqlx_error) => sqlx_error
86                .as_database_error()
87                .is_some_and(|db_error| db_error.code().as_deref() == Some("42P13")),
88            _ => false,
89        }
90    }
91
92    pub fn constraint<T: Into<String>>(message: T) -> Self {
93        Self::Constraint(message.into())
94    }
95
96    pub fn invalid_argument<T: Into<String>>(message: T) -> Self {
97        Self::InvalidArgument(message.into())
98    }
99
100    pub fn internal<T: Into<String>>(message: T) -> Self {
101        Self::Internal(message.into())
102    }
103
104    pub fn invalid_state<T: Into<String>>(message: T) -> Self {
105        Self::InvalidState(message.into())
106    }
107
108    #[must_use]
109    pub const fn is_not_found(&self) -> bool {
110        matches!(self, Self::NotFound(_))
111    }
112
113    #[must_use]
114    pub const fn is_constraint(&self) -> bool {
115        matches!(self, Self::Constraint(_))
116    }
117}
118
119impl From<RepositoryError> for systemprompt_traits::RepositoryError {
120    fn from(err: RepositoryError) -> Self {
121        Self::database(err)
122    }
123}