Skip to main content

sqlmodel_core/
error.rs

1//! Error types for SQLModel operations.
2
3use std::fmt;
4
5/// The primary error type for all SQLModel operations.
6#[derive(Debug)]
7pub enum Error {
8    /// Connection-related errors (connect, disconnect, timeout)
9    Connection(ConnectionError),
10    /// Query execution errors
11    Query(QueryError),
12    /// Type conversion errors
13    Type(TypeError),
14    /// Transaction errors
15    Transaction(TransactionError),
16    /// Protocol errors (wire-level)
17    Protocol(ProtocolError),
18    /// Pool errors
19    Pool(PoolError),
20    /// Schema/migration errors
21    Schema(SchemaError),
22    /// Configuration errors
23    Config(ConfigError),
24    /// Validation errors
25    Validation(ValidationError),
26    /// I/O errors
27    Io(std::io::Error),
28    /// Operation timed out
29    Timeout,
30    /// Operation was cancelled via asupersync
31    Cancelled,
32    /// Serialization/deserialization errors
33    Serde(String),
34    /// Custom error with message
35    Custom(String),
36}
37
38#[derive(Debug)]
39pub struct ConnectionError {
40    pub kind: ConnectionErrorKind,
41    pub message: String,
42    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ConnectionErrorKind {
47    /// Failed to establish connection
48    Connect,
49    /// Authentication failed
50    Authentication,
51    /// Connection lost during operation
52    Disconnected,
53    /// SSL/TLS negotiation failed
54    Ssl,
55    /// DNS resolution failed
56    DnsResolution,
57    /// Connection refused
58    Refused,
59    /// Connection pool exhausted
60    PoolExhausted,
61}
62
63#[derive(Debug)]
64pub struct QueryError {
65    pub kind: QueryErrorKind,
66    pub sql: Option<String>,
67    pub sqlstate: Option<String>,
68    pub message: String,
69    pub detail: Option<String>,
70    pub hint: Option<String>,
71    pub position: Option<usize>,
72    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum QueryErrorKind {
77    /// Syntax error in SQL
78    Syntax,
79    /// Constraint violation (unique, foreign key, etc.)
80    Constraint,
81    /// Table or column not found
82    NotFound,
83    /// Permission denied
84    Permission,
85    /// Data too large for column
86    DataTruncation,
87    /// Deadlock detected
88    Deadlock,
89    /// Serialization failure (retry may succeed)
90    Serialization,
91    /// Statement timeout
92    Timeout,
93    /// Cancelled
94    Cancelled,
95    /// Other database error
96    Database,
97}
98
99#[derive(Debug)]
100pub struct TypeError {
101    pub expected: &'static str,
102    pub actual: String,
103    pub column: Option<String>,
104    pub rust_type: Option<&'static str>,
105}
106
107#[derive(Debug)]
108pub struct TransactionError {
109    pub kind: TransactionErrorKind,
110    pub message: String,
111}
112
113#[derive(Debug, Clone, Copy)]
114pub enum TransactionErrorKind {
115    /// Already committed
116    AlreadyCommitted,
117    /// Already rolled back
118    AlreadyRolledBack,
119    /// Savepoint not found
120    SavepointNotFound,
121    /// Nested transaction not supported
122    NestedNotSupported,
123}
124
125#[derive(Debug)]
126pub struct ProtocolError {
127    pub message: String,
128    pub raw_data: Option<Vec<u8>>,
129    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
130}
131
132#[derive(Debug)]
133pub struct PoolError {
134    pub kind: PoolErrorKind,
135    pub message: String,
136    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
137}
138
139impl PoolError {
140    /// Create a new pool error indicating the internal mutex was poisoned.
141    ///
142    /// The `operation` parameter should describe what operation was being attempted
143    /// when the poisoned lock was encountered (e.g., "acquire", "close", "stats").
144    pub fn poisoned(operation: &str) -> Self {
145        Self {
146            kind: PoolErrorKind::Poisoned,
147            message: format!(
148                "pool mutex poisoned during {operation}; a thread panicked while holding the lock"
149            ),
150            source: None,
151        }
152    }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PoolErrorKind {
157    /// Pool exhausted (no available connections)
158    Exhausted,
159    /// Connection checkout timeout
160    Timeout,
161    /// Pool is closed
162    Closed,
163    /// Configuration error
164    Config,
165    /// Internal mutex was poisoned (a thread panicked while holding the lock)
166    ///
167    /// This indicates a serious internal error. The pool may still be usable
168    /// for read-only operations, but mutation operations will fail.
169    Poisoned,
170}
171
172#[derive(Debug)]
173pub struct SchemaError {
174    pub kind: SchemaErrorKind,
175    pub message: String,
176    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
177}
178
179#[derive(Debug, Clone, Copy)]
180pub enum SchemaErrorKind {
181    /// Table already exists
182    TableExists,
183    /// Table not found
184    TableNotFound,
185    /// Column already exists
186    ColumnExists,
187    /// Column not found
188    ColumnNotFound,
189    /// Invalid schema definition
190    Invalid,
191    /// Migration error
192    Migration,
193}
194
195#[derive(Debug)]
196pub struct ConfigError {
197    pub message: String,
198    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
199}
200
201/// Validation error for field-level and model-level validation.
202#[derive(Debug, Clone)]
203pub struct ValidationError {
204    /// The errors grouped by field name (or "_model" for model-level)
205    pub errors: Vec<FieldValidationError>,
206}
207
208/// A single validation error for a field.
209#[derive(Debug, Clone)]
210pub struct FieldValidationError {
211    /// The field name that failed validation
212    pub field: String,
213    /// The kind of validation that failed
214    pub kind: ValidationErrorKind,
215    /// Human-readable error message
216    pub message: String,
217}
218
219/// The type of validation constraint that was violated.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ValidationErrorKind {
222    /// Value is below minimum
223    Min,
224    /// Value is above maximum
225    Max,
226    /// String is shorter than minimum length
227    MinLength,
228    /// String is longer than maximum length
229    MaxLength,
230    /// Value doesn't match regex pattern
231    Pattern,
232    /// Required field is missing/null
233    Required,
234    /// Custom validation failed
235    Custom,
236    /// Model-level validation failed
237    Model,
238    /// Value is not a multiple of the specified divisor
239    MultipleOf,
240    /// Collection has fewer items than minimum
241    MinItems,
242    /// Collection has more items than maximum
243    MaxItems,
244    /// Collection contains duplicate items
245    UniqueItems,
246    /// Invalid credit card number (Luhn check failed)
247    CreditCard,
248}
249
250impl ValidationError {
251    /// Create a new empty validation error container.
252    pub fn new() -> Self {
253        Self { errors: Vec::new() }
254    }
255
256    /// Check if there are any validation errors.
257    pub fn is_empty(&self) -> bool {
258        self.errors.is_empty()
259    }
260
261    /// Add a field validation error.
262    pub fn add(
263        &mut self,
264        field: impl Into<String>,
265        kind: ValidationErrorKind,
266        message: impl Into<String>,
267    ) {
268        self.errors.push(FieldValidationError {
269            field: field.into(),
270            kind,
271            message: message.into(),
272        });
273    }
274
275    /// Add a min value error.
276    pub fn add_min(
277        &mut self,
278        field: impl Into<String>,
279        min: impl std::fmt::Display,
280        actual: impl std::fmt::Display,
281    ) {
282        self.add(
283            field,
284            ValidationErrorKind::Min,
285            format!("must be at least {min}, got {actual}"),
286        );
287    }
288
289    /// Add a max value error.
290    pub fn add_max(
291        &mut self,
292        field: impl Into<String>,
293        max: impl std::fmt::Display,
294        actual: impl std::fmt::Display,
295    ) {
296        self.add(
297            field,
298            ValidationErrorKind::Max,
299            format!("must be at most {max}, got {actual}"),
300        );
301    }
302
303    /// Add a multiple_of error.
304    ///
305    /// Used when a numeric value is not a multiple of the specified divisor.
306    pub fn add_multiple_of(
307        &mut self,
308        field: impl Into<String>,
309        divisor: impl std::fmt::Display,
310        actual: impl std::fmt::Display,
311    ) {
312        self.add(
313            field,
314            ValidationErrorKind::MultipleOf,
315            format!("must be a multiple of {divisor}, got {actual}"),
316        );
317    }
318
319    /// Add a min_items error for collections.
320    ///
321    /// Used when a collection has fewer items than the minimum required.
322    pub fn add_min_items(&mut self, field: impl Into<String>, min: usize, actual: usize) {
323        self.add(
324            field,
325            ValidationErrorKind::MinItems,
326            format!("must have at least {min} items, got {actual}"),
327        );
328    }
329
330    /// Add a max_items error for collections.
331    ///
332    /// Used when a collection has more items than the maximum allowed.
333    pub fn add_max_items(&mut self, field: impl Into<String>, max: usize, actual: usize) {
334        self.add(
335            field,
336            ValidationErrorKind::MaxItems,
337            format!("must have at most {max} items, got {actual}"),
338        );
339    }
340
341    /// Add a unique_items error for collections.
342    ///
343    /// Used when a collection contains duplicate items.
344    pub fn add_unique_items(&mut self, field: impl Into<String>, duplicate_count: usize) {
345        self.add(
346            field,
347            ValidationErrorKind::UniqueItems,
348            format!("must have unique items, found {duplicate_count} duplicate(s)"),
349        );
350    }
351
352    /// Add a min length error.
353    pub fn add_min_length(&mut self, field: impl Into<String>, min: usize, actual: usize) {
354        self.add(
355            field,
356            ValidationErrorKind::MinLength,
357            format!("must be at least {min} characters, got {actual}"),
358        );
359    }
360
361    /// Add a max length error.
362    pub fn add_max_length(&mut self, field: impl Into<String>, max: usize, actual: usize) {
363        self.add(
364            field,
365            ValidationErrorKind::MaxLength,
366            format!("must be at most {max} characters, got {actual}"),
367        );
368    }
369
370    /// Add a pattern match error.
371    pub fn add_pattern(&mut self, field: impl Into<String>, pattern: &str) {
372        self.add(
373            field,
374            ValidationErrorKind::Pattern,
375            format!("must match pattern '{pattern}'"),
376        );
377    }
378
379    /// Add a required field error.
380    pub fn add_required(&mut self, field: impl Into<String>) {
381        self.add(
382            field,
383            ValidationErrorKind::Required,
384            "is required".to_string(),
385        );
386    }
387
388    /// Add a custom validation error.
389    pub fn add_custom(&mut self, field: impl Into<String>, message: impl Into<String>) {
390        self.add(field, ValidationErrorKind::Custom, message);
391    }
392
393    /// Add a model-level validation error.
394    ///
395    /// Model-level validators check cross-field constraints or validate the
396    /// entire model state. The error is recorded with field "__model__".
397    pub fn add_model_error(&mut self, message: impl Into<String>) {
398        self.add("__model__", ValidationErrorKind::Model, message);
399    }
400
401    /// Add a credit card validation error.
402    pub fn add_credit_card(&mut self, field: impl Into<String>) {
403        self.add(
404            field,
405            ValidationErrorKind::CreditCard,
406            "is not a valid credit card number".to_string(),
407        );
408    }
409
410    /// Convert to Result, returning Ok(()) if no errors, Err(self) otherwise.
411    pub fn into_result(self) -> std::result::Result<(), Self> {
412        if self.is_empty() { Ok(()) } else { Err(self) }
413    }
414}
415
416impl Default for ValidationError {
417    fn default() -> Self {
418        Self::new()
419    }
420}
421
422impl Error {
423    /// Is this a retryable error (deadlock, serialization, pool exhausted, timeouts)?
424    pub fn is_retryable(&self) -> bool {
425        match self {
426            Error::Query(q) => matches!(
427                q.kind,
428                QueryErrorKind::Deadlock | QueryErrorKind::Serialization | QueryErrorKind::Timeout
429            ),
430            Error::Pool(p) => matches!(p.kind, PoolErrorKind::Exhausted | PoolErrorKind::Timeout),
431            Error::Connection(c) => matches!(c.kind, ConnectionErrorKind::PoolExhausted),
432            Error::Timeout => true,
433            _ => false,
434        }
435    }
436
437    /// Is this error due to a poisoned mutex in the connection pool?
438    ///
439    /// A poisoned mutex indicates a thread panicked while holding the lock.
440    /// This is a serious internal error and the pool may be in an inconsistent state.
441    pub fn is_pool_poisoned(&self) -> bool {
442        matches!(self, Error::Pool(p) if p.kind == PoolErrorKind::Poisoned)
443    }
444
445    /// Is this a connection error that likely requires reconnection?
446    pub fn is_connection_error(&self) -> bool {
447        match self {
448            Error::Connection(c) => matches!(
449                c.kind,
450                ConnectionErrorKind::Connect
451                    | ConnectionErrorKind::Authentication
452                    | ConnectionErrorKind::Disconnected
453                    | ConnectionErrorKind::Ssl
454                    | ConnectionErrorKind::DnsResolution
455                    | ConnectionErrorKind::Refused
456            ),
457            Error::Protocol(_) | Error::Io(_) => true,
458            _ => false,
459        }
460    }
461
462    /// Get SQLSTATE if available (e.g., "23505" for unique violation)
463    pub fn sqlstate(&self) -> Option<&str> {
464        match self {
465            Error::Query(q) => q.sqlstate.as_deref(),
466            _ => None,
467        }
468    }
469
470    /// Get the SQL that caused this error, if available
471    pub fn sql(&self) -> Option<&str> {
472        match self {
473            Error::Query(q) => q.sql.as_deref(),
474            _ => None,
475        }
476    }
477}
478
479impl QueryError {
480    /// Is this a unique constraint violation?
481    pub fn is_unique_violation(&self) -> bool {
482        self.sqlstate.as_deref() == Some("23505")
483    }
484
485    /// Is this a foreign key violation?
486    pub fn is_foreign_key_violation(&self) -> bool {
487        self.sqlstate.as_deref() == Some("23503")
488    }
489}
490
491impl fmt::Display for Error {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        match self {
494            Error::Connection(e) => write!(f, "Connection error: {}", e.message),
495            Error::Query(e) => {
496                if let Some(sqlstate) = &e.sqlstate {
497                    write!(f, "Query error (SQLSTATE {}): {}", sqlstate, e.message)
498                } else {
499                    write!(f, "Query error: {}", e.message)
500                }
501            }
502            Error::Type(e) => {
503                if let Some(col) = &e.column {
504                    write!(
505                        f,
506                        "Type error in column '{}': expected {}, found {}",
507                        col, e.expected, e.actual
508                    )
509                } else {
510                    write!(f, "Type error: expected {}, found {}", e.expected, e.actual)
511                }
512            }
513            Error::Transaction(e) => write!(f, "Transaction error: {}", e.message),
514            Error::Protocol(e) => write!(f, "Protocol error: {}", e.message),
515            Error::Pool(e) => write!(f, "Pool error: {}", e.message),
516            Error::Schema(e) => write!(f, "Schema error: {}", e.message),
517            Error::Config(e) => write!(f, "Configuration error: {}", e.message),
518            Error::Validation(e) => write!(f, "Validation error: {}", e),
519            Error::Io(e) => write!(f, "I/O error: {}", e),
520            Error::Timeout => write!(f, "Operation timed out"),
521            Error::Cancelled => write!(f, "Operation cancelled"),
522            Error::Serde(msg) => write!(f, "Serialization error: {}", msg),
523            Error::Custom(msg) => write!(f, "{}", msg),
524        }
525    }
526}
527
528impl std::error::Error for Error {
529    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
530        match self {
531            Error::Connection(e) => e
532                .source
533                .as_deref()
534                .map(|err| err as &(dyn std::error::Error + 'static)),
535            Error::Query(e) => e
536                .source
537                .as_deref()
538                .map(|err| err as &(dyn std::error::Error + 'static)),
539            Error::Protocol(e) => e
540                .source
541                .as_deref()
542                .map(|err| err as &(dyn std::error::Error + 'static)),
543            Error::Pool(e) => e
544                .source
545                .as_deref()
546                .map(|err| err as &(dyn std::error::Error + 'static)),
547            Error::Schema(e) => e
548                .source
549                .as_deref()
550                .map(|err| err as &(dyn std::error::Error + 'static)),
551            Error::Config(e) => e
552                .source
553                .as_deref()
554                .map(|err| err as &(dyn std::error::Error + 'static)),
555            Error::Io(e) => Some(e),
556            _ => None,
557        }
558    }
559}
560
561impl fmt::Display for ConnectionError {
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        write!(f, "{}", self.message)
564    }
565}
566
567impl fmt::Display for QueryError {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        if let Some(sqlstate) = &self.sqlstate {
570            write!(f, "{} (SQLSTATE {})", self.message, sqlstate)
571        } else {
572            write!(f, "{}", self.message)
573        }
574    }
575}
576
577impl fmt::Display for TypeError {
578    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579        if let Some(col) = &self.column {
580            write!(
581                f,
582                "expected {} for column '{}', found {}",
583                self.expected, col, self.actual
584            )
585        } else {
586            write!(f, "expected {}, found {}", self.expected, self.actual)
587        }
588    }
589}
590
591impl fmt::Display for TransactionError {
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        write!(f, "{}", self.message)
594    }
595}
596
597impl fmt::Display for ProtocolError {
598    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599        write!(f, "{}", self.message)
600    }
601}
602
603impl fmt::Display for PoolError {
604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605        write!(f, "{}", self.message)
606    }
607}
608
609impl fmt::Display for SchemaError {
610    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611        write!(f, "{}", self.message)
612    }
613}
614
615impl fmt::Display for ConfigError {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        write!(f, "{}", self.message)
618    }
619}
620
621impl fmt::Display for ValidationError {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        if self.errors.is_empty() {
624            write!(f, "validation passed")
625        } else if self.errors.len() == 1 {
626            let err = &self.errors[0];
627            write!(f, "validation error on '{}': {}", err.field, err.message)
628        } else {
629            writeln!(f, "validation errors:")?;
630            for err in &self.errors {
631                writeln!(f, "  - {}: {}", err.field, err.message)?;
632            }
633            Ok(())
634        }
635    }
636}
637
638impl std::error::Error for ValidationError {}
639
640impl From<std::io::Error> for Error {
641    fn from(err: std::io::Error) -> Self {
642        Error::Io(err)
643    }
644}
645
646impl From<ConnectionError> for Error {
647    fn from(err: ConnectionError) -> Self {
648        Error::Connection(err)
649    }
650}
651
652impl From<QueryError> for Error {
653    fn from(err: QueryError) -> Self {
654        Error::Query(err)
655    }
656}
657
658impl From<TypeError> for Error {
659    fn from(err: TypeError) -> Self {
660        Error::Type(err)
661    }
662}
663
664impl From<TransactionError> for Error {
665    fn from(err: TransactionError) -> Self {
666        Error::Transaction(err)
667    }
668}
669
670impl From<ProtocolError> for Error {
671    fn from(err: ProtocolError) -> Self {
672        Error::Protocol(err)
673    }
674}
675
676impl From<PoolError> for Error {
677    fn from(err: PoolError) -> Self {
678        Error::Pool(err)
679    }
680}
681
682impl From<SchemaError> for Error {
683    fn from(err: SchemaError) -> Self {
684        Error::Schema(err)
685    }
686}
687
688impl From<ConfigError> for Error {
689    fn from(err: ConfigError) -> Self {
690        Error::Config(err)
691    }
692}
693
694impl From<ValidationError> for Error {
695    fn from(err: ValidationError) -> Self {
696        Error::Validation(err)
697    }
698}
699
700/// Result type alias for SQLModel operations.
701pub type Result<T> = std::result::Result<T, Error>;
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn sqlstate_helpers() {
709        let query = QueryError {
710            kind: QueryErrorKind::Constraint,
711            sql: Some("SELECT 1".to_string()),
712            sqlstate: Some("23505".to_string()),
713            message: "unique violation".to_string(),
714            detail: None,
715            hint: None,
716            position: None,
717            source: None,
718        };
719
720        assert!(query.is_unique_violation());
721        assert!(!query.is_foreign_key_violation());
722
723        let err = Error::Query(query);
724        assert_eq!(err.sqlstate(), Some("23505"));
725        assert_eq!(err.sql(), Some("SELECT 1"));
726    }
727
728    #[test]
729    fn retryable_and_connection_flags() {
730        let retryable_query = Error::Query(QueryError {
731            kind: QueryErrorKind::Deadlock,
732            sql: None,
733            sqlstate: None,
734            message: "deadlock detected".to_string(),
735            detail: None,
736            hint: None,
737            position: None,
738            source: None,
739        });
740
741        let pool_exhausted = Error::Pool(PoolError {
742            kind: PoolErrorKind::Exhausted,
743            message: "pool exhausted".to_string(),
744            source: None,
745        });
746
747        let conn_exhausted = Error::Connection(ConnectionError {
748            kind: ConnectionErrorKind::PoolExhausted,
749            message: "pool exhausted".to_string(),
750            source: None,
751        });
752
753        assert!(retryable_query.is_retryable());
754        assert!(pool_exhausted.is_retryable());
755        assert!(conn_exhausted.is_retryable());
756
757        let conn_error = Error::Connection(ConnectionError {
758            kind: ConnectionErrorKind::Disconnected,
759            message: "lost connection".to_string(),
760            source: None,
761        });
762        assert!(conn_error.is_connection_error());
763    }
764
765    #[test]
766    fn pool_poisoned_error() {
767        // Test the convenience constructor
768        let err = PoolError::poisoned("acquire");
769        assert_eq!(err.kind, PoolErrorKind::Poisoned);
770        assert!(err.message.contains("acquire"));
771        assert!(err.message.contains("poisoned"));
772        assert!(err.message.contains("panicked"));
773
774        // Test wrapped in Error
775        let error = Error::Pool(err);
776        assert!(error.is_pool_poisoned());
777        assert!(!error.is_retryable()); // Poisoned errors are NOT retryable
778        assert!(!error.is_connection_error());
779
780        // Test Display
781        let display = format!("{}", error);
782        assert!(display.contains("Pool error"));
783        assert!(display.contains("poisoned"));
784    }
785
786    #[test]
787    fn pool_poisoned_not_retryable() {
788        let poisoned = Error::Pool(PoolError::poisoned("close"));
789        let exhausted = Error::Pool(PoolError {
790            kind: PoolErrorKind::Exhausted,
791            message: "no connections".to_string(),
792            source: None,
793        });
794        let timeout = Error::Pool(PoolError {
795            kind: PoolErrorKind::Timeout,
796            message: "timed out".to_string(),
797            source: None,
798        });
799
800        // Poisoned is NOT retryable (it's a permanent failure)
801        assert!(!poisoned.is_retryable());
802        // But exhausted and timeout ARE retryable
803        assert!(exhausted.is_retryable());
804        assert!(timeout.is_retryable());
805    }
806}