oxify_storage/
error.rs

1//! Error types for storage operations
2
3use thiserror::Error;
4use uuid::Uuid;
5
6/// Result type for storage operations
7pub type Result<T> = std::result::Result<T, StorageError>;
8
9/// Resource type for not found errors
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ResourceType {
13    User,
14    Workflow,
15    WorkflowVersion,
16    Execution,
17    ApiKey,
18    Secret,
19    Quota,
20    Schedule,
21    Webhook,
22    Checkpoint,
23    AuditLog,
24    Metrics,
25    Other(String),
26}
27
28impl std::fmt::Display for ResourceType {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            ResourceType::User => write!(f, "User"),
32            ResourceType::Workflow => write!(f, "Workflow"),
33            ResourceType::WorkflowVersion => write!(f, "WorkflowVersion"),
34            ResourceType::Execution => write!(f, "Execution"),
35            ResourceType::ApiKey => write!(f, "ApiKey"),
36            ResourceType::Secret => write!(f, "Secret"),
37            ResourceType::Quota => write!(f, "Quota"),
38            ResourceType::Schedule => write!(f, "Schedule"),
39            ResourceType::Webhook => write!(f, "Webhook"),
40            ResourceType::Checkpoint => write!(f, "Checkpoint"),
41            ResourceType::AuditLog => write!(f, "AuditLog"),
42            ResourceType::Metrics => write!(f, "Metrics"),
43            ResourceType::Other(s) => write!(f, "{s}"),
44        }
45    }
46}
47
48/// Resource identifier that can be a UUID, string, or other type
49#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum ResourceId {
52    Uuid(Uuid),
53    String(String),
54    Composite(Vec<String>),
55}
56
57impl std::fmt::Display for ResourceId {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            ResourceId::Uuid(id) => write!(f, "{id}"),
61            ResourceId::String(s) => write!(f, "{s}"),
62            ResourceId::Composite(parts) => write!(f, "{}", parts.join(":")),
63        }
64    }
65}
66
67impl From<Uuid> for ResourceId {
68    fn from(id: Uuid) -> Self {
69        ResourceId::Uuid(id)
70    }
71}
72
73impl From<String> for ResourceId {
74    fn from(s: String) -> Self {
75        ResourceId::String(s)
76    }
77}
78
79impl From<&str> for ResourceId {
80    fn from(s: &str) -> Self {
81        ResourceId::String(s.to_string())
82    }
83}
84
85/// Errors that can occur during storage operations
86#[derive(Error, Debug)]
87pub enum StorageError {
88    /// Database error
89    #[error("Database error: {0}")]
90    Database(#[from] sqlx::Error),
91
92    /// Serialization error
93    #[error("Serialization error: {0}")]
94    Serialization(#[from] serde_json::Error),
95
96    /// Not found error with detailed context
97    #[error("{resource_type} not found: {resource_id}")]
98    NotFound {
99        resource_type: ResourceType,
100        resource_id: ResourceId,
101    },
102
103    /// Legacy not found error (for backwards compatibility)
104    #[error("Resource not found: {0}")]
105    NotFoundLegacy(String),
106
107    /// Constraint violation
108    #[error("Constraint violation: {0}")]
109    ConstraintViolation(String),
110
111    /// Concurrent modification detected (optimistic locking)
112    #[error("Concurrent modification: {0}")]
113    ConcurrentModification(String),
114
115    /// Migration error
116    #[error("Migration error: {0}")]
117    Migration(String),
118
119    /// Encryption error
120    #[error("Encryption error: {0}")]
121    EncryptionError(String),
122
123    /// Validation error
124    #[error("Validation error: {0}")]
125    ValidationError(String),
126
127    /// Backup/restore error
128    #[error("Backup error: {0}")]
129    BackupError(String),
130
131    /// Batch operation error
132    #[error("Batch size {size} exceeds maximum {max}")]
133    BatchTooLarge { size: usize, max: usize },
134}
135
136impl StorageError {
137    /// Create a not found error with resource type and ID
138    pub fn not_found<T: Into<ResourceId>>(resource_type: ResourceType, resource_id: T) -> Self {
139        StorageError::NotFound {
140            resource_type,
141            resource_id: resource_id.into(),
142        }
143    }
144
145    /// Create a constraint violation error
146    pub fn constraint_violation<S: Into<String>>(message: S) -> Self {
147        StorageError::ConstraintViolation(message.into())
148    }
149
150    /// Create a validation error
151    pub fn validation<S: Into<String>>(message: S) -> Self {
152        StorageError::ValidationError(message.into())
153    }
154
155    /// Create an encryption error
156    pub fn encryption<S: Into<String>>(message: S) -> Self {
157        StorageError::EncryptionError(message.into())
158    }
159
160    /// Create a migration error
161    pub fn migration<S: Into<String>>(message: S) -> Self {
162        StorageError::Migration(message.into())
163    }
164
165    /// Create a backup error
166    pub fn backup<S: Into<String>>(message: S) -> Self {
167        StorageError::BackupError(message.into())
168    }
169
170    /// Check if this is a not found error
171    pub fn is_not_found(&self) -> bool {
172        matches!(
173            self,
174            StorageError::NotFound { .. } | StorageError::NotFoundLegacy(_)
175        )
176    }
177
178    /// Check if this is a constraint violation error
179    pub fn is_constraint_violation(&self) -> bool {
180        matches!(self, StorageError::ConstraintViolation(_))
181    }
182
183    /// Check if this is a validation error
184    pub fn is_validation_error(&self) -> bool {
185        matches!(self, StorageError::ValidationError(_))
186    }
187
188    /// Check if this is a database error
189    pub fn is_database_error(&self) -> bool {
190        matches!(self, StorageError::Database(_))
191    }
192
193    /// Get the resource type if this is a not found error
194    pub fn resource_type(&self) -> Option<&ResourceType> {
195        match self {
196            StorageError::NotFound { resource_type, .. } => Some(resource_type),
197            _ => None,
198        }
199    }
200
201    /// Get the resource ID if this is a not found error
202    pub fn resource_id(&self) -> Option<&ResourceId> {
203        match self {
204            StorageError::NotFound { resource_id, .. } => Some(resource_id),
205            _ => None,
206        }
207    }
208
209    /// Check if the error is retryable (transient database errors)
210    pub fn is_retryable(&self) -> bool {
211        match self {
212            StorageError::Database(e) => {
213                // Check for connection pool errors (always retryable)
214                if matches!(e, sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed) {
215                    return true;
216                }
217
218                // Check for I/O errors (connection issues)
219                if matches!(e, sqlx::Error::Io(_)) {
220                    return true;
221                }
222
223                // Check for transient database-specific errors
224                e.as_database_error()
225                    .and_then(sqlx::error::DatabaseError::code)
226                    .is_some_and(|code| {
227                        // PostgreSQL error codes for transient errors:
228                        // 40001 - serialization_failure
229                        // 40P01 - deadlock_detected
230                        // 08006 - connection_failure
231                        // 08003 - connection_does_not_exist
232                        // 57P03 - cannot_connect_now
233                        matches!(
234                            code.as_ref(),
235                            "40001" | "40P01" | "08006" | "08003" | "57P03"
236                        )
237                    })
238            }
239            _ => false,
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_not_found_error_creation() {
250        let error = StorageError::not_found(ResourceType::Workflow, Uuid::nil());
251        assert!(error.is_not_found());
252        assert_eq!(error.resource_type(), Some(&ResourceType::Workflow));
253    }
254
255    #[test]
256    fn test_validation_error_creation() {
257        let error = StorageError::validation("Invalid input");
258        assert!(error.is_validation_error());
259        assert!(!error.is_not_found());
260    }
261
262    #[test]
263    fn test_constraint_violation_error_creation() {
264        let error = StorageError::constraint_violation("Unique constraint violated");
265        assert!(error.is_constraint_violation());
266    }
267
268    #[test]
269    fn test_resource_type_serialization() {
270        let resource_type = ResourceType::Workflow;
271        let json = serde_json::to_string(&resource_type).unwrap();
272        assert_eq!(json, "\"workflow\"");
273
274        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
275        assert_eq!(deserialized, ResourceType::Workflow);
276    }
277
278    #[test]
279    fn test_resource_id_serialization() {
280        let resource_id = ResourceId::String("test-123".to_string());
281        let json = serde_json::to_string(&resource_id).unwrap();
282        let deserialized: ResourceId = serde_json::from_str(&json).unwrap();
283        assert_eq!(deserialized, resource_id);
284    }
285
286    #[test]
287    fn test_resource_id_from_uuid() {
288        let uuid = Uuid::nil();
289        let resource_id: ResourceId = uuid.into();
290        matches!(resource_id, ResourceId::Uuid(_));
291    }
292
293    #[test]
294    fn test_resource_id_from_string() {
295        let resource_id: ResourceId = "test".into();
296        matches!(resource_id, ResourceId::String(_));
297    }
298
299    #[test]
300    fn test_error_helper_methods() {
301        let migration_err = StorageError::migration("Migration failed");
302        assert!(!migration_err.is_not_found());
303        assert!(!migration_err.is_validation_error());
304
305        let encryption_err = StorageError::encryption("Encryption failed");
306        assert!(!encryption_err.is_constraint_violation());
307
308        let backup_err = StorageError::backup("Backup failed");
309        assert!(!backup_err.is_database_error());
310    }
311}