prax_query/
error.rs

1//! Comprehensive error types for query operations with actionable messages.
2//!
3//! This module provides detailed error types that include:
4//! - Error codes for programmatic handling
5//! - Actionable suggestions for fixing issues
6//! - Context about what operation failed
7//! - Help text and documentation links
8//!
9//! # Error Codes
10//!
11//! Error codes follow a pattern: P{category}{number}
12//! - 1xxx: Query errors (not found, invalid filter, etc.)
13//! - 2xxx: Constraint violations (unique, foreign key, etc.)
14//! - 3xxx: Connection errors (timeout, pool, auth)
15//! - 4xxx: Transaction errors (deadlock, serialization)
16//! - 5xxx: Execution errors (timeout, syntax, params)
17//! - 6xxx: Data errors (type, serialization)
18//! - 7xxx: Configuration errors
19//! - 8xxx: Migration errors
20//! - 9xxx: Tenant errors
21//!
22//! ```rust
23//! use prax_query::ErrorCode;
24//!
25//! // Error codes have string representations
26//! let code = ErrorCode::RecordNotFound;
27//! let code = ErrorCode::UniqueConstraint;
28//! let code = ErrorCode::ConnectionFailed;
29//! ```
30//!
31//! # Creating Errors
32//!
33//! ```rust
34//! use prax_query::{QueryError, ErrorCode};
35//!
36//! // Not found error
37//! let err = QueryError::not_found("User");
38//! assert_eq!(err.code, ErrorCode::RecordNotFound);
39//!
40//! // Generic error with code
41//! let err = QueryError::new(ErrorCode::UniqueConstraint, "Email already exists");
42//! assert_eq!(err.code, ErrorCode::UniqueConstraint);
43//! ```
44//!
45//! # Error Properties
46//!
47//! ```rust
48//! use prax_query::{QueryError, ErrorCode};
49//!
50//! let err = QueryError::not_found("User");
51//!
52//! // Access error code (public field)
53//! assert_eq!(err.code, ErrorCode::RecordNotFound);
54//!
55//! // Access error message
56//! let message = err.to_string();
57//! assert!(message.contains("User"));
58//! ```
59
60use std::fmt;
61use thiserror::Error;
62
63/// Result type for query operations.
64pub type QueryResult<T> = Result<T, QueryError>;
65
66/// Error codes for programmatic error handling.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum ErrorCode {
69    // Query errors (1xxx)
70    /// Record not found (P1001).
71    RecordNotFound = 1001,
72    /// Multiple records found when expecting one (P1002).
73    NotUnique = 1002,
74    /// Invalid filter or where clause (P1003).
75    InvalidFilter = 1003,
76    /// Invalid select or include (P1004).
77    InvalidSelect = 1004,
78    /// Required field missing (P1005).
79    RequiredFieldMissing = 1005,
80
81    // Constraint errors (2xxx)
82    /// Unique constraint violation (P2001).
83    UniqueConstraint = 2001,
84    /// Foreign key constraint violation (P2002).
85    ForeignKeyConstraint = 2002,
86    /// Check constraint violation (P2003).
87    CheckConstraint = 2003,
88    /// Not null constraint violation (P2004).
89    NotNullConstraint = 2004,
90
91    // Connection errors (3xxx)
92    /// Database connection failed (P3001).
93    ConnectionFailed = 3001,
94    /// Connection pool exhausted (P3002).
95    PoolExhausted = 3002,
96    /// Connection timeout (P3003).
97    ConnectionTimeout = 3003,
98    /// Authentication failed (P3004).
99    AuthenticationFailed = 3004,
100    /// SSL/TLS error (P3005).
101    SslError = 3005,
102
103    // Transaction errors (4xxx)
104    /// Transaction failed (P4001).
105    TransactionFailed = 4001,
106    /// Deadlock detected (P4002).
107    Deadlock = 4002,
108    /// Serialization failure (P4003).
109    SerializationFailure = 4003,
110    /// Transaction already committed/rolled back (P4004).
111    TransactionClosed = 4004,
112
113    // Query execution errors (5xxx)
114    /// Query timeout (P5001).
115    QueryTimeout = 5001,
116    /// SQL syntax error (P5002).
117    SqlSyntax = 5002,
118    /// Invalid parameter (P5003).
119    InvalidParameter = 5003,
120    /// Query too complex (P5004).
121    QueryTooComplex = 5004,
122    /// General database error (P5005).
123    DatabaseError = 5005,
124
125    // Data errors (6xxx)
126    /// Invalid data type (P6001).
127    InvalidDataType = 6001,
128    /// Serialization error (P6002).
129    SerializationError = 6002,
130    /// Deserialization error (P6003).
131    DeserializationError = 6003,
132    /// Data truncation (P6004).
133    DataTruncation = 6004,
134
135    // Configuration errors (7xxx)
136    /// Invalid configuration (P7001).
137    InvalidConfiguration = 7001,
138    /// Missing configuration (P7002).
139    MissingConfiguration = 7002,
140    /// Invalid connection string (P7003).
141    InvalidConnectionString = 7003,
142
143    // Internal errors (9xxx)
144    /// Internal error (P9001).
145    Internal = 9001,
146    /// Unknown error (P9999).
147    Unknown = 9999,
148}
149
150impl ErrorCode {
151    /// Get the error code string (e.g., "P1001").
152    pub fn code(&self) -> String {
153        format!("P{}", *self as u16)
154    }
155
156    /// Get a short description of the error code.
157    pub fn description(&self) -> &'static str {
158        match self {
159            Self::RecordNotFound => "Record not found",
160            Self::NotUnique => "Multiple records found",
161            Self::InvalidFilter => "Invalid filter condition",
162            Self::InvalidSelect => "Invalid select or include",
163            Self::RequiredFieldMissing => "Required field missing",
164            Self::UniqueConstraint => "Unique constraint violation",
165            Self::ForeignKeyConstraint => "Foreign key constraint violation",
166            Self::CheckConstraint => "Check constraint violation",
167            Self::NotNullConstraint => "Not null constraint violation",
168            Self::ConnectionFailed => "Database connection failed",
169            Self::PoolExhausted => "Connection pool exhausted",
170            Self::ConnectionTimeout => "Connection timeout",
171            Self::AuthenticationFailed => "Authentication failed",
172            Self::SslError => "SSL/TLS error",
173            Self::TransactionFailed => "Transaction failed",
174            Self::Deadlock => "Deadlock detected",
175            Self::SerializationFailure => "Serialization failure",
176            Self::TransactionClosed => "Transaction already closed",
177            Self::QueryTimeout => "Query timeout",
178            Self::SqlSyntax => "SQL syntax error",
179            Self::InvalidParameter => "Invalid parameter",
180            Self::QueryTooComplex => "Query too complex",
181            Self::DatabaseError => "Database error",
182            Self::InvalidDataType => "Invalid data type",
183            Self::SerializationError => "Serialization error",
184            Self::DeserializationError => "Deserialization error",
185            Self::DataTruncation => "Data truncation",
186            Self::InvalidConfiguration => "Invalid configuration",
187            Self::MissingConfiguration => "Missing configuration",
188            Self::InvalidConnectionString => "Invalid connection string",
189            Self::Internal => "Internal error",
190            Self::Unknown => "Unknown error",
191        }
192    }
193
194    /// Get the documentation URL for this error.
195    pub fn docs_url(&self) -> String {
196        format!("https://prax.rs/docs/errors/{}", self.code())
197    }
198}
199
200impl fmt::Display for ErrorCode {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "{}", self.code())
203    }
204}
205
206/// Suggestion for fixing an error.
207#[derive(Debug, Clone)]
208pub struct Suggestion {
209    /// The suggestion text.
210    pub text: String,
211    /// Optional code example.
212    pub code: Option<String>,
213}
214
215impl Suggestion {
216    /// Create a new suggestion.
217    pub fn new(text: impl Into<String>) -> Self {
218        Self {
219            text: text.into(),
220            code: None,
221        }
222    }
223
224    /// Add a code example.
225    pub fn with_code(mut self, code: impl Into<String>) -> Self {
226        self.code = Some(code.into());
227        self
228    }
229}
230
231/// Additional context for an error.
232#[derive(Debug, Clone, Default)]
233pub struct ErrorContext {
234    /// The operation that was being performed.
235    pub operation: Option<String>,
236    /// The model involved.
237    pub model: Option<String>,
238    /// The field involved.
239    pub field: Option<String>,
240    /// The SQL query (if available).
241    pub sql: Option<String>,
242    /// Suggestions for fixing the error.
243    pub suggestions: Vec<Suggestion>,
244    /// Help text.
245    pub help: Option<String>,
246    /// Related errors.
247    pub related: Vec<String>,
248}
249
250impl ErrorContext {
251    /// Create new empty context.
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Set the operation.
257    pub fn operation(mut self, op: impl Into<String>) -> Self {
258        self.operation = Some(op.into());
259        self
260    }
261
262    /// Set the model.
263    pub fn model(mut self, model: impl Into<String>) -> Self {
264        self.model = Some(model.into());
265        self
266    }
267
268    /// Set the field.
269    pub fn field(mut self, field: impl Into<String>) -> Self {
270        self.field = Some(field.into());
271        self
272    }
273
274    /// Set the SQL query.
275    pub fn sql(mut self, sql: impl Into<String>) -> Self {
276        self.sql = Some(sql.into());
277        self
278    }
279
280    /// Add a suggestion.
281    pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
282        self.suggestions.push(suggestion);
283        self
284    }
285
286    /// Add a text suggestion.
287    pub fn suggest(mut self, text: impl Into<String>) -> Self {
288        self.suggestions.push(Suggestion::new(text));
289        self
290    }
291
292    /// Set help text.
293    pub fn help(mut self, help: impl Into<String>) -> Self {
294        self.help = Some(help.into());
295        self
296    }
297}
298
299/// Errors that can occur during query operations.
300#[derive(Error, Debug)]
301pub struct QueryError {
302    /// The error code.
303    pub code: ErrorCode,
304    /// The error message.
305    pub message: String,
306    /// Additional context.
307    pub context: ErrorContext,
308    /// The source error (if any).
309    #[source]
310    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
311}
312
313impl fmt::Display for QueryError {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "[{}] {}", self.code.code(), self.message)
316    }
317}
318
319impl QueryError {
320    /// Create a new error with the given code and message.
321    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
322        Self {
323            code,
324            message: message.into(),
325            context: ErrorContext::default(),
326            source: None,
327        }
328    }
329
330    /// Add context about the operation.
331    pub fn with_context(mut self, operation: impl Into<String>) -> Self {
332        self.context.operation = Some(operation.into());
333        self
334    }
335
336    /// Add a suggestion for fixing the error.
337    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
338        self.context.suggestions.push(Suggestion::new(suggestion));
339        self
340    }
341
342    /// Add a code suggestion.
343    pub fn with_code_suggestion(mut self, text: impl Into<String>, code: impl Into<String>) -> Self {
344        self.context.suggestions.push(Suggestion::new(text).with_code(code));
345        self
346    }
347
348    /// Add help text.
349    pub fn with_help(mut self, help: impl Into<String>) -> Self {
350        self.context.help = Some(help.into());
351        self
352    }
353
354    /// Set the model.
355    pub fn with_model(mut self, model: impl Into<String>) -> Self {
356        self.context.model = Some(model.into());
357        self
358    }
359
360    /// Set the field.
361    pub fn with_field(mut self, field: impl Into<String>) -> Self {
362        self.context.field = Some(field.into());
363        self
364    }
365
366    /// Set the SQL query.
367    pub fn with_sql(mut self, sql: impl Into<String>) -> Self {
368        self.context.sql = Some(sql.into());
369        self
370    }
371
372    /// Set the source error.
373    pub fn with_source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
374        self.source = Some(Box::new(source));
375        self
376    }
377
378    // ============== Constructor Functions ==============
379
380    /// Create a not found error.
381    pub fn not_found(model: impl Into<String>) -> Self {
382        let model = model.into();
383        Self::new(
384            ErrorCode::RecordNotFound,
385            format!("No {} record found matching the query", model),
386        )
387        .with_model(&model)
388        .with_suggestion(format!("Verify the {} exists before querying", model))
389        .with_code_suggestion(
390            "Use findFirst() instead to get None instead of an error",
391            format!("client.{}().find_first().r#where(...).exec().await", model.to_lowercase()),
392        )
393    }
394
395    /// Create a not unique error.
396    pub fn not_unique(model: impl Into<String>) -> Self {
397        let model = model.into();
398        Self::new(
399            ErrorCode::NotUnique,
400            format!("Expected unique {} record but found multiple", model),
401        )
402        .with_model(&model)
403        .with_suggestion("Add more specific filters to narrow down to a single record")
404        .with_suggestion("Use find_many() if you expect multiple results")
405    }
406
407    /// Create a constraint violation error.
408    pub fn constraint_violation(model: impl Into<String>, message: impl Into<String>) -> Self {
409        let model = model.into();
410        let message = message.into();
411        Self::new(
412            ErrorCode::UniqueConstraint,
413            format!("Constraint violation on {}: {}", model, message),
414        )
415        .with_model(&model)
416    }
417
418    /// Create a unique constraint violation error.
419    pub fn unique_violation(model: impl Into<String>, field: impl Into<String>) -> Self {
420        let model = model.into();
421        let field = field.into();
422        Self::new(
423            ErrorCode::UniqueConstraint,
424            format!("Unique constraint violated on {}.{}", model, field),
425        )
426        .with_model(&model)
427        .with_field(&field)
428        .with_suggestion(format!("A record with this {} already exists", field))
429        .with_code_suggestion(
430            "Use upsert() to update if exists, create if not",
431            format!(
432                "client.{}().upsert()\n  .r#where({}::{}::equals(value))\n  .create(...)\n  .update(...)\n  .exec().await",
433                model.to_lowercase(), model.to_lowercase(), field
434            ),
435        )
436    }
437
438    /// Create a foreign key violation error.
439    pub fn foreign_key_violation(model: impl Into<String>, relation: impl Into<String>) -> Self {
440        let model = model.into();
441        let relation = relation.into();
442        Self::new(
443            ErrorCode::ForeignKeyConstraint,
444            format!("Foreign key constraint violated: {} -> {}", model, relation),
445        )
446        .with_model(&model)
447        .with_field(&relation)
448        .with_suggestion(format!("Ensure the related {} record exists before creating this {}", relation, model))
449        .with_suggestion("Check for typos in the relation ID")
450    }
451
452    /// Create a not null violation error.
453    pub fn not_null_violation(model: impl Into<String>, field: impl Into<String>) -> Self {
454        let model = model.into();
455        let field = field.into();
456        Self::new(
457            ErrorCode::NotNullConstraint,
458            format!("Cannot set {}.{} to null - field is required", model, field),
459        )
460        .with_model(&model)
461        .with_field(&field)
462        .with_suggestion(format!("Provide a value for the {} field", field))
463        .with_help("Make the field optional in your schema if null should be allowed")
464    }
465
466    /// Create an invalid input error.
467    pub fn invalid_input(field: impl Into<String>, message: impl Into<String>) -> Self {
468        let field = field.into();
469        let message = message.into();
470        Self::new(
471            ErrorCode::InvalidParameter,
472            format!("Invalid input for {}: {}", field, message),
473        )
474        .with_field(&field)
475    }
476
477    /// Create a connection error.
478    pub fn connection(message: impl Into<String>) -> Self {
479        let message = message.into();
480        Self::new(ErrorCode::ConnectionFailed, format!("Connection error: {}", message))
481            .with_suggestion("Check that the database server is running")
482            .with_suggestion("Verify the connection URL is correct")
483            .with_suggestion("Check firewall settings allow the connection")
484    }
485
486    /// Create a connection timeout error.
487    pub fn connection_timeout(duration_ms: u64) -> Self {
488        Self::new(
489            ErrorCode::ConnectionTimeout,
490            format!("Connection timed out after {}ms", duration_ms),
491        )
492        .with_suggestion("Increase the connect_timeout in your connection string")
493        .with_suggestion("Check network connectivity to the database server")
494        .with_code_suggestion(
495            "Add connect_timeout to your connection URL",
496            "postgres://user:pass@host/db?connect_timeout=30",
497        )
498    }
499
500    /// Create a pool exhausted error.
501    pub fn pool_exhausted(max_connections: u32) -> Self {
502        Self::new(
503            ErrorCode::PoolExhausted,
504            format!("Connection pool exhausted (max {} connections)", max_connections),
505        )
506        .with_suggestion("Increase max_connections in pool configuration")
507        .with_suggestion("Ensure connections are being released properly")
508        .with_suggestion("Check for connection leaks in your application")
509        .with_help("Consider using connection pooling middleware like PgBouncer for high-traffic applications")
510    }
511
512    /// Create an authentication error.
513    pub fn authentication_failed(message: impl Into<String>) -> Self {
514        let message = message.into();
515        Self::new(ErrorCode::AuthenticationFailed, format!("Authentication failed: {}", message))
516            .with_suggestion("Check username and password in connection string")
517            .with_suggestion("Verify the user has permission to access the database")
518            .with_suggestion("Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)")
519    }
520
521    /// Create a timeout error.
522    pub fn timeout(duration_ms: u64) -> Self {
523        Self::new(
524            ErrorCode::QueryTimeout,
525            format!("Query timed out after {}ms", duration_ms),
526        )
527        .with_suggestion("Optimize the query to run faster")
528        .with_suggestion("Add indexes to improve query performance")
529        .with_suggestion("Increase the query timeout if the query is expected to be slow")
530        .with_help("Consider paginating large result sets")
531    }
532
533    /// Create a transaction error.
534    pub fn transaction(message: impl Into<String>) -> Self {
535        let message = message.into();
536        Self::new(ErrorCode::TransactionFailed, format!("Transaction error: {}", message))
537    }
538
539    /// Create a deadlock error.
540    pub fn deadlock() -> Self {
541        Self::new(ErrorCode::Deadlock, "Deadlock detected - transaction was rolled back".to_string())
542            .with_suggestion("Retry the transaction")
543            .with_suggestion("Access tables in a consistent order across transactions")
544            .with_suggestion("Keep transactions short to reduce lock contention")
545            .with_help("Deadlocks occur when two transactions wait for each other's locks")
546    }
547
548    /// Create an SQL syntax error.
549    pub fn sql_syntax(message: impl Into<String>, sql: impl Into<String>) -> Self {
550        let message = message.into();
551        let sql = sql.into();
552        Self::new(ErrorCode::SqlSyntax, format!("SQL syntax error: {}", message))
553            .with_sql(&sql)
554            .with_suggestion("Check the generated SQL for errors")
555            .with_help("This is likely a bug in Prax - please report it")
556    }
557
558    /// Create a serialization error.
559    pub fn serialization(message: impl Into<String>) -> Self {
560        Self::new(ErrorCode::SerializationError, message.into())
561    }
562
563    /// Create a deserialization error.
564    pub fn deserialization(message: impl Into<String>) -> Self {
565        let message = message.into();
566        Self::new(ErrorCode::DeserializationError, format!("Failed to deserialize result: {}", message))
567            .with_suggestion("Check that the model matches the database schema")
568            .with_suggestion("Ensure data types are compatible")
569    }
570
571    /// Create a general database error.
572    pub fn database(message: impl Into<String>) -> Self {
573        let message = message.into();
574        Self::new(ErrorCode::DatabaseError, message)
575            .with_suggestion("Check the database logs for more details")
576    }
577
578    /// Create an internal error.
579    pub fn internal(message: impl Into<String>) -> Self {
580        let message = message.into();
581        Self::new(ErrorCode::Internal, format!("Internal error: {}", message))
582            .with_help("This is likely a bug in Prax - please report it at https://github.com/pegasusheavy/prax/issues")
583    }
584
585    // ============== Error Checks ==============
586
587    /// Check if this is a not found error.
588    pub fn is_not_found(&self) -> bool {
589        self.code == ErrorCode::RecordNotFound
590    }
591
592    /// Check if this is a constraint violation.
593    pub fn is_constraint_violation(&self) -> bool {
594        matches!(
595            self.code,
596            ErrorCode::UniqueConstraint
597                | ErrorCode::ForeignKeyConstraint
598                | ErrorCode::CheckConstraint
599                | ErrorCode::NotNullConstraint
600        )
601    }
602
603    /// Check if this is a timeout error.
604    pub fn is_timeout(&self) -> bool {
605        matches!(self.code, ErrorCode::QueryTimeout | ErrorCode::ConnectionTimeout)
606    }
607
608    /// Check if this is a connection error.
609    pub fn is_connection_error(&self) -> bool {
610        matches!(
611            self.code,
612            ErrorCode::ConnectionFailed
613                | ErrorCode::PoolExhausted
614                | ErrorCode::ConnectionTimeout
615                | ErrorCode::AuthenticationFailed
616                | ErrorCode::SslError
617        )
618    }
619
620    /// Check if this error is retryable.
621    pub fn is_retryable(&self) -> bool {
622        matches!(
623            self.code,
624            ErrorCode::ConnectionTimeout
625                | ErrorCode::PoolExhausted
626                | ErrorCode::QueryTimeout
627                | ErrorCode::Deadlock
628                | ErrorCode::SerializationFailure
629        )
630    }
631
632    // ============== Display Functions ==============
633
634    /// Get the error code.
635    pub fn error_code(&self) -> &ErrorCode {
636        &self.code
637    }
638
639    /// Get the documentation URL for this error.
640    pub fn docs_url(&self) -> String {
641        self.code.docs_url()
642    }
643
644    /// Display the full error with all context and suggestions.
645    pub fn display_full(&self) -> String {
646        let mut output = String::new();
647
648        // Error header
649        output.push_str(&format!("Error [{}]: {}\n", self.code.code(), self.message));
650
651        // Context
652        if let Some(ref op) = self.context.operation {
653            output.push_str(&format!("  → While: {}\n", op));
654        }
655        if let Some(ref model) = self.context.model {
656            output.push_str(&format!("  → Model: {}\n", model));
657        }
658        if let Some(ref field) = self.context.field {
659            output.push_str(&format!("  → Field: {}\n", field));
660        }
661
662        // SQL (truncated if too long)
663        if let Some(ref sql) = self.context.sql {
664            let sql_display = if sql.len() > 200 {
665                format!("{}...", &sql[..200])
666            } else {
667                sql.clone()
668            };
669            output.push_str(&format!("  → SQL: {}\n", sql_display));
670        }
671
672        // Suggestions
673        if !self.context.suggestions.is_empty() {
674            output.push_str("\nSuggestions:\n");
675            for (i, suggestion) in self.context.suggestions.iter().enumerate() {
676                output.push_str(&format!("  {}. {}\n", i + 1, suggestion.text));
677                if let Some(ref code) = suggestion.code {
678                    output.push_str(&format!("     ```\n     {}\n     ```\n", code.replace('\n', "\n     ")));
679                }
680            }
681        }
682
683        // Help
684        if let Some(ref help) = self.context.help {
685            output.push_str(&format!("\nHelp: {}\n", help));
686        }
687
688        // Documentation link
689        output.push_str(&format!("\nMore info: {}\n", self.docs_url()));
690
691        output
692    }
693
694    /// Display error with ANSI colors for terminal output.
695    pub fn display_colored(&self) -> String {
696        let mut output = String::new();
697
698        // Error header (red)
699        output.push_str(&format!(
700            "\x1b[1;31mError [{}]\x1b[0m: \x1b[1m{}\x1b[0m\n",
701            self.code.code(),
702            self.message
703        ));
704
705        // Context (dim)
706        if let Some(ref op) = self.context.operation {
707            output.push_str(&format!("  \x1b[2m→ While:\x1b[0m {}\n", op));
708        }
709        if let Some(ref model) = self.context.model {
710            output.push_str(&format!("  \x1b[2m→ Model:\x1b[0m {}\n", model));
711        }
712        if let Some(ref field) = self.context.field {
713            output.push_str(&format!("  \x1b[2m→ Field:\x1b[0m {}\n", field));
714        }
715
716        // Suggestions (yellow)
717        if !self.context.suggestions.is_empty() {
718            output.push_str("\n\x1b[1;33mSuggestions:\x1b[0m\n");
719            for (i, suggestion) in self.context.suggestions.iter().enumerate() {
720                output.push_str(&format!("  \x1b[33m{}.\x1b[0m {}\n", i + 1, suggestion.text));
721                if let Some(ref code) = suggestion.code {
722                    output.push_str(&format!(
723                        "     \x1b[2m```\x1b[0m\n     \x1b[36m{}\x1b[0m\n     \x1b[2m```\x1b[0m\n",
724                        code.replace('\n', "\n     ")
725                    ));
726                }
727            }
728        }
729
730        // Help (cyan)
731        if let Some(ref help) = self.context.help {
732            output.push_str(&format!("\n\x1b[1;36mHelp:\x1b[0m {}\n", help));
733        }
734
735        // Documentation link (blue)
736        output.push_str(&format!(
737            "\n\x1b[2mMore info:\x1b[0m \x1b[4;34m{}\x1b[0m\n",
738            self.docs_url()
739        ));
740
741        output
742    }
743}
744
745/// Extension trait for converting errors to QueryError.
746pub trait IntoQueryError {
747    /// Convert to a QueryError.
748    fn into_query_error(self) -> QueryError;
749}
750
751impl<E: std::error::Error + Send + Sync + 'static> IntoQueryError for E {
752    fn into_query_error(self) -> QueryError {
753        QueryError::internal(self.to_string()).with_source(self)
754    }
755}
756
757/// Helper for creating errors with context.
758#[macro_export]
759macro_rules! query_error {
760    ($code:expr, $msg:expr) => {
761        $crate::error::QueryError::new($code, $msg)
762    };
763    ($code:expr, $msg:expr, $($key:ident = $value:expr),+ $(,)?) => {{
764        let mut err = $crate::error::QueryError::new($code, $msg);
765        $(
766            err = err.$key($value);
767        )+
768        err
769    }};
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    #[test]
777    fn test_error_code_format() {
778        assert_eq!(ErrorCode::RecordNotFound.code(), "P1001");
779        assert_eq!(ErrorCode::UniqueConstraint.code(), "P2001");
780        assert_eq!(ErrorCode::ConnectionFailed.code(), "P3001");
781    }
782
783    #[test]
784    fn test_not_found_error() {
785        let err = QueryError::not_found("User");
786        assert!(err.is_not_found());
787        assert!(err.message.contains("User"));
788        assert!(!err.context.suggestions.is_empty());
789    }
790
791    #[test]
792    fn test_unique_violation_error() {
793        let err = QueryError::unique_violation("User", "email");
794        assert!(err.is_constraint_violation());
795        assert_eq!(err.context.model, Some("User".to_string()));
796        assert_eq!(err.context.field, Some("email".to_string()));
797    }
798
799    #[test]
800    fn test_timeout_error() {
801        let err = QueryError::timeout(5000);
802        assert!(err.is_timeout());
803        assert!(err.message.contains("5000"));
804    }
805
806    #[test]
807    fn test_error_with_context() {
808        let err = QueryError::not_found("User")
809            .with_context("Finding user by email")
810            .with_suggestion("Use a different query method");
811
812        assert_eq!(err.context.operation, Some("Finding user by email".to_string()));
813        assert!(err.context.suggestions.len() >= 2); // Original + new one
814    }
815
816    #[test]
817    fn test_retryable_errors() {
818        assert!(QueryError::timeout(1000).is_retryable());
819        assert!(QueryError::deadlock().is_retryable());
820        assert!(QueryError::pool_exhausted(10).is_retryable());
821        assert!(!QueryError::not_found("User").is_retryable());
822    }
823
824    #[test]
825    fn test_connection_errors() {
826        assert!(QueryError::connection("failed").is_connection_error());
827        assert!(QueryError::authentication_failed("bad password").is_connection_error());
828        assert!(QueryError::pool_exhausted(10).is_connection_error());
829    }
830
831    #[test]
832    fn test_display_full() {
833        let err = QueryError::unique_violation("User", "email")
834            .with_context("Creating new user");
835
836        let output = err.display_full();
837        assert!(output.contains("P2001"));
838        assert!(output.contains("User"));
839        assert!(output.contains("email"));
840        assert!(output.contains("Suggestions"));
841    }
842
843    #[test]
844    fn test_docs_url() {
845        let err = QueryError::not_found("User");
846        assert!(err.docs_url().contains("P1001"));
847    }
848
849    #[test]
850    fn test_error_macro() {
851        let err = query_error!(
852            ErrorCode::InvalidParameter,
853            "Invalid email format",
854            with_field = "email",
855            with_suggestion = "Use a valid email address"
856        );
857
858        assert_eq!(err.code, ErrorCode::InvalidParameter);
859        assert_eq!(err.context.field, Some("email".to_string()));
860    }
861
862    #[test]
863    fn test_suggestion_with_code() {
864        let err = QueryError::not_found("User")
865            .with_code_suggestion("Try this instead", "client.user().find_first()");
866
867        let suggestion = err.context.suggestions.last().unwrap();
868        assert!(suggestion.code.is_some());
869    }
870}