Skip to main content

nautilus_connector/
error.rs

1//! Error types for the Nautilus connector layer.
2//!
3//! Runtime execution failures (database errors, connection failures, row-decoding
4//! problems) are represented here as [`ConnectorError`], keeping `nautilus-core`
5//! free of any dependency on database driver concepts.
6
7/// Classification of the underlying `sqlx::Error` discriminant.
8///
9/// Enables programmatic inspection of the original error category without
10/// storing the non-`Clone` `sqlx::Error` itself.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SqlxErrorKind {
13    /// Not originating from sqlx (e.g. logic errors, type mismatches).
14    None,
15    /// A database-level error (constraint violation, syntax error, etc.).
16    Database,
17    /// Unique constraint violation (e.g. duplicate key).
18    UniqueConstraint,
19    /// Foreign key constraint violation.
20    ForeignKeyConstraint,
21    /// Check constraint violation.
22    CheckConstraint,
23    /// NOT NULL constraint violation (inserting NULL into a non-nullable column).
24    NullConstraint,
25    /// Deadlock detected between concurrent transactions.
26    Deadlock,
27    /// Serialization failure — transaction must be retried.
28    SerializationFailure,
29    /// I/O error during communication.
30    Io,
31    /// TLS handshake failure.
32    Tls,
33    /// Protocol-level error.
34    Protocol,
35    /// Expected row was not found.
36    RowNotFound,
37    /// Requested type not found in the database.
38    TypeNotFound,
39    /// Column index out of bounds.
40    ColumnIndexOutOfBounds,
41    /// Named column not found.
42    ColumnNotFound,
43    /// Column decode failure.
44    ColumnDecode,
45    /// General decode failure.
46    Decode,
47    /// Connection pool timed out.
48    PoolTimedOut,
49    /// Connection pool was closed.
50    PoolClosed,
51    /// A background pool worker crashed.
52    WorkerCrashed,
53    /// Configuration error.
54    Configuration,
55}
56
57impl SqlxErrorKind {
58    /// Classify a `sqlx::Error` into its corresponding kind.
59    pub fn from_sqlx(e: &sqlx::Error) -> Self {
60        match e {
61            sqlx::Error::Database(db_err) => {
62                if db_err.is_unique_violation() {
63                    SqlxErrorKind::UniqueConstraint
64                } else if db_err.is_foreign_key_violation() {
65                    SqlxErrorKind::ForeignKeyConstraint
66                } else if db_err.is_check_violation() {
67                    SqlxErrorKind::CheckConstraint
68                } else {
69                    // Detect NOT NULL, deadlock, and serialization failures via SQLState
70                    // (PostgreSQL) or error message patterns (MySQL, SQLite).
71                    let state = db_err.code();
72                    let state = state.as_deref().unwrap_or("");
73                    let msg = db_err.message();
74                    if state == "23502"
75                        || msg.contains("NOT NULL constraint")
76                        || msg.contains("not-null constraint")
77                        || msg.contains("cannot be null")
78                    {
79                        SqlxErrorKind::NullConstraint
80                    } else if state == "40P01" || msg.to_ascii_lowercase().contains("deadlock") {
81                        SqlxErrorKind::Deadlock
82                    } else if state == "40001"
83                        || msg.to_ascii_lowercase().contains("serialization failure")
84                        || msg.to_ascii_lowercase().contains("could not serialize")
85                    {
86                        SqlxErrorKind::SerializationFailure
87                    } else {
88                        SqlxErrorKind::Database
89                    }
90                }
91            }
92            sqlx::Error::Io(_) => SqlxErrorKind::Io,
93            sqlx::Error::Tls(_) => SqlxErrorKind::Tls,
94            sqlx::Error::Protocol(_) => SqlxErrorKind::Protocol,
95            sqlx::Error::RowNotFound => SqlxErrorKind::RowNotFound,
96            sqlx::Error::TypeNotFound { .. } => SqlxErrorKind::TypeNotFound,
97            sqlx::Error::ColumnIndexOutOfBounds { .. } => SqlxErrorKind::ColumnIndexOutOfBounds,
98            sqlx::Error::ColumnNotFound(_) => SqlxErrorKind::ColumnNotFound,
99            sqlx::Error::ColumnDecode { .. } => SqlxErrorKind::ColumnDecode,
100            sqlx::Error::Decode(_) => SqlxErrorKind::Decode,
101            sqlx::Error::PoolTimedOut => SqlxErrorKind::PoolTimedOut,
102            sqlx::Error::PoolClosed => SqlxErrorKind::PoolClosed,
103            sqlx::Error::WorkerCrashed => SqlxErrorKind::WorkerCrashed,
104            sqlx::Error::Configuration(_) => SqlxErrorKind::Configuration,
105            // sqlx::Error is #[non_exhaustive]; new variants default to None
106            #[allow(unreachable_patterns)]
107            _ => SqlxErrorKind::None,
108        }
109    }
110}
111
112/// Error type for database connector operations.
113///
114/// This covers everything that can go wrong at *runtime* when talking to a
115/// database. Query-building errors are represented by [`nautilus_core::Error`]
116/// and can be wrapped via the [`ConnectorError::Core`] variant.
117///
118/// Each variant that originates from a sqlx error carries a [`SqlxErrorKind`]
119/// discriminant for programmatic inspection (e.g. constraint violations vs I/O
120/// errors) without storing the non-`Clone` `sqlx::Error` itself.
121#[derive(Debug, Clone, PartialEq, thiserror::Error)]
122pub enum ConnectorError {
123    /// A query was executed successfully but the database returned an error.
124    #[error("Database error: {1}")]
125    Database(SqlxErrorKind, String),
126    /// Could not establish or acquire a database connection.
127    #[error("Connection error: {1}")]
128    Connection(SqlxErrorKind, String),
129    /// A row could not be decoded into the expected Rust types.
130    #[error("Row decode error: {1}")]
131    RowDecode(SqlxErrorKind, String),
132    /// A query-building error originating from `nautilus-core`.
133    #[error("Core error: {0}")]
134    Core(#[from] nautilus_core::Error),
135}
136
137impl ConnectorError {
138    /// Create a `Database` error from a sqlx error with a context message.
139    pub fn database(e: sqlx::Error, context: &str) -> Self {
140        ConnectorError::Database(SqlxErrorKind::from_sqlx(&e), format!("{}: {}", context, e))
141    }
142
143    /// Create a `Connection` error from a sqlx error with a context message.
144    pub fn connection(e: sqlx::Error, context: &str) -> Self {
145        ConnectorError::Connection(SqlxErrorKind::from_sqlx(&e), format!("{}: {}", context, e))
146    }
147
148    /// Create a `RowDecode` error from a sqlx error with a context message.
149    pub fn row_decode(e: sqlx::Error, context: &str) -> Self {
150        ConnectorError::RowDecode(SqlxErrorKind::from_sqlx(&e), format!("{}: {}", context, e))
151    }
152
153    /// Create a `Database` error from a plain message (no sqlx source).
154    pub fn database_msg(msg: impl Into<String>) -> Self {
155        ConnectorError::Database(SqlxErrorKind::None, msg.into())
156    }
157
158    /// Create a `Connection` error from a plain message (no sqlx source).
159    pub fn connection_msg(msg: impl Into<String>) -> Self {
160        ConnectorError::Connection(SqlxErrorKind::None, msg.into())
161    }
162
163    /// Create a `RowDecode` error from a plain message (no sqlx source).
164    pub fn row_decode_msg(msg: impl Into<String>) -> Self {
165        ConnectorError::RowDecode(SqlxErrorKind::None, msg.into())
166    }
167
168    /// Returns the [`SqlxErrorKind`] for this error, if applicable.
169    pub fn sqlx_kind(&self) -> SqlxErrorKind {
170        match self {
171            ConnectorError::Database(k, _)
172            | ConnectorError::Connection(k, _)
173            | ConnectorError::RowDecode(k, _) => *k,
174            ConnectorError::Core(_) => SqlxErrorKind::None,
175        }
176    }
177}
178
179/// Result type alias for connector operations.
180pub type Result<T> = std::result::Result<T, ConnectorError>;
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_display() {
188        assert_eq!(
189            ConnectorError::database_msg("query failed").to_string(),
190            "Database error: query failed"
191        );
192        assert_eq!(
193            ConnectorError::connection_msg("refused").to_string(),
194            "Connection error: refused"
195        );
196        assert_eq!(
197            ConnectorError::row_decode_msg("invalid bool").to_string(),
198            "Row decode error: invalid bool"
199        );
200    }
201
202    #[test]
203    fn test_sqlx_kind() {
204        let err = ConnectorError::database_msg("test");
205        assert_eq!(err.sqlx_kind(), SqlxErrorKind::None);
206
207        let err = ConnectorError::Database(SqlxErrorKind::PoolTimedOut, "timeout".to_string());
208        assert_eq!(err.sqlx_kind(), SqlxErrorKind::PoolTimedOut);
209    }
210
211    #[test]
212    fn test_from_core_error() {
213        let core_err = nautilus_core::Error::InvalidQuery("bad query".to_string());
214        let conn_err = ConnectorError::from(core_err.clone());
215        assert_eq!(conn_err, ConnectorError::Core(core_err));
216        assert!(conn_err.to_string().contains("bad query"));
217    }
218}