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(
344        mut self,
345        text: impl Into<String>,
346        code: impl Into<String>,
347    ) -> Self {
348        self.context
349            .suggestions
350            .push(Suggestion::new(text).with_code(code));
351        self
352    }
353
354    /// Add help text.
355    pub fn with_help(mut self, help: impl Into<String>) -> Self {
356        self.context.help = Some(help.into());
357        self
358    }
359
360    /// Set the model.
361    pub fn with_model(mut self, model: impl Into<String>) -> Self {
362        self.context.model = Some(model.into());
363        self
364    }
365
366    /// Set the field.
367    pub fn with_field(mut self, field: impl Into<String>) -> Self {
368        self.context.field = Some(field.into());
369        self
370    }
371
372    /// Set the SQL query.
373    pub fn with_sql(mut self, sql: impl Into<String>) -> Self {
374        self.context.sql = Some(sql.into());
375        self
376    }
377
378    /// Set the source error.
379    pub fn with_source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
380        self.source = Some(Box::new(source));
381        self
382    }
383
384    // ============== Constructor Functions ==============
385
386    /// Create a not found error.
387    pub fn not_found(model: impl Into<String>) -> Self {
388        let model = model.into();
389        Self::new(
390            ErrorCode::RecordNotFound,
391            format!("No {} record found matching the query", model),
392        )
393        .with_model(&model)
394        .with_suggestion(format!("Verify the {} exists before querying", model))
395        .with_code_suggestion(
396            "Use findFirst() instead to get None instead of an error",
397            format!(
398                "client.{}().find_first().r#where(...).exec().await",
399                model.to_lowercase()
400            ),
401        )
402    }
403
404    /// Create a not unique error.
405    pub fn not_unique(model: impl Into<String>) -> Self {
406        let model = model.into();
407        Self::new(
408            ErrorCode::NotUnique,
409            format!("Expected unique {} record but found multiple", model),
410        )
411        .with_model(&model)
412        .with_suggestion("Add more specific filters to narrow down to a single record")
413        .with_suggestion("Use find_many() if you expect multiple results")
414    }
415
416    /// Create a constraint violation error.
417    pub fn constraint_violation(model: impl Into<String>, message: impl Into<String>) -> Self {
418        let model = model.into();
419        let message = message.into();
420        Self::new(
421            ErrorCode::UniqueConstraint,
422            format!("Constraint violation on {}: {}", model, message),
423        )
424        .with_model(&model)
425    }
426
427    /// Create a unique constraint violation error.
428    pub fn unique_violation(model: impl Into<String>, field: impl Into<String>) -> Self {
429        let model = model.into();
430        let field = field.into();
431        Self::new(
432            ErrorCode::UniqueConstraint,
433            format!("Unique constraint violated on {}.{}", model, field),
434        )
435        .with_model(&model)
436        .with_field(&field)
437        .with_suggestion(format!("A record with this {} already exists", field))
438        .with_code_suggestion(
439            "Use upsert() to update if exists, create if not",
440            format!(
441                "client.{}().upsert()\n  .r#where({}::{}::equals(value))\n  .create(...)\n  .update(...)\n  .exec().await",
442                model.to_lowercase(), model.to_lowercase(), field
443            ),
444        )
445    }
446
447    /// Create a foreign key violation error.
448    pub fn foreign_key_violation(model: impl Into<String>, relation: impl Into<String>) -> Self {
449        let model = model.into();
450        let relation = relation.into();
451        Self::new(
452            ErrorCode::ForeignKeyConstraint,
453            format!("Foreign key constraint violated: {} -> {}", model, relation),
454        )
455        .with_model(&model)
456        .with_field(&relation)
457        .with_suggestion(format!(
458            "Ensure the related {} record exists before creating this {}",
459            relation, model
460        ))
461        .with_suggestion("Check for typos in the relation ID")
462    }
463
464    /// Create a not null violation error.
465    pub fn not_null_violation(model: impl Into<String>, field: impl Into<String>) -> Self {
466        let model = model.into();
467        let field = field.into();
468        Self::new(
469            ErrorCode::NotNullConstraint,
470            format!("Cannot set {}.{} to null - field is required", model, field),
471        )
472        .with_model(&model)
473        .with_field(&field)
474        .with_suggestion(format!("Provide a value for the {} field", field))
475        .with_help("Make the field optional in your schema if null should be allowed")
476    }
477
478    /// Create an invalid input error.
479    pub fn invalid_input(field: impl Into<String>, message: impl Into<String>) -> Self {
480        let field = field.into();
481        let message = message.into();
482        Self::new(
483            ErrorCode::InvalidParameter,
484            format!("Invalid input for {}: {}", field, message),
485        )
486        .with_field(&field)
487    }
488
489    /// Create a connection error.
490    pub fn connection(message: impl Into<String>) -> Self {
491        let message = message.into();
492        Self::new(
493            ErrorCode::ConnectionFailed,
494            format!("Connection error: {}", message),
495        )
496        .with_suggestion("Check that the database server is running")
497        .with_suggestion("Verify the connection URL is correct")
498        .with_suggestion("Check firewall settings allow the connection")
499    }
500
501    /// Create a connection timeout error.
502    pub fn connection_timeout(duration_ms: u64) -> Self {
503        Self::new(
504            ErrorCode::ConnectionTimeout,
505            format!("Connection timed out after {}ms", duration_ms),
506        )
507        .with_suggestion("Increase the connect_timeout in your connection string")
508        .with_suggestion("Check network connectivity to the database server")
509        .with_code_suggestion(
510            "Add connect_timeout to your connection URL",
511            "postgres://user:pass@host/db?connect_timeout=30",
512        )
513    }
514
515    /// Create a pool exhausted error.
516    pub fn pool_exhausted(max_connections: u32) -> Self {
517        Self::new(
518            ErrorCode::PoolExhausted,
519            format!("Connection pool exhausted (max {} connections)", max_connections),
520        )
521        .with_suggestion("Increase max_connections in pool configuration")
522        .with_suggestion("Ensure connections are being released properly")
523        .with_suggestion("Check for connection leaks in your application")
524        .with_help("Consider using connection pooling middleware like PgBouncer for high-traffic applications")
525    }
526
527    /// Create an authentication error.
528    pub fn authentication_failed(message: impl Into<String>) -> Self {
529        let message = message.into();
530        Self::new(
531            ErrorCode::AuthenticationFailed,
532            format!("Authentication failed: {}", message),
533        )
534        .with_suggestion("Check username and password in connection string")
535        .with_suggestion("Verify the user has permission to access the database")
536        .with_suggestion("Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)")
537    }
538
539    /// Create a timeout error.
540    pub fn timeout(duration_ms: u64) -> Self {
541        Self::new(
542            ErrorCode::QueryTimeout,
543            format!("Query timed out after {}ms", duration_ms),
544        )
545        .with_suggestion("Optimize the query to run faster")
546        .with_suggestion("Add indexes to improve query performance")
547        .with_suggestion("Increase the query timeout if the query is expected to be slow")
548        .with_help("Consider paginating large result sets")
549    }
550
551    /// Create a transaction error.
552    pub fn transaction(message: impl Into<String>) -> Self {
553        let message = message.into();
554        Self::new(
555            ErrorCode::TransactionFailed,
556            format!("Transaction error: {}", message),
557        )
558    }
559
560    /// Create a deadlock error.
561    pub fn deadlock() -> Self {
562        Self::new(
563            ErrorCode::Deadlock,
564            "Deadlock detected - transaction was rolled back".to_string(),
565        )
566        .with_suggestion("Retry the transaction")
567        .with_suggestion("Access tables in a consistent order across transactions")
568        .with_suggestion("Keep transactions short to reduce lock contention")
569        .with_help("Deadlocks occur when two transactions wait for each other's locks")
570    }
571
572    /// Create an SQL syntax error.
573    pub fn sql_syntax(message: impl Into<String>, sql: impl Into<String>) -> Self {
574        let message = message.into();
575        let sql = sql.into();
576        Self::new(
577            ErrorCode::SqlSyntax,
578            format!("SQL syntax error: {}", message),
579        )
580        .with_sql(&sql)
581        .with_suggestion("Check the generated SQL for errors")
582        .with_help("This is likely a bug in Prax - please report it")
583    }
584
585    /// Create a serialization error.
586    pub fn serialization(message: impl Into<String>) -> Self {
587        Self::new(ErrorCode::SerializationError, message.into())
588    }
589
590    /// Create a deserialization error.
591    pub fn deserialization(message: impl Into<String>) -> Self {
592        let message = message.into();
593        Self::new(
594            ErrorCode::DeserializationError,
595            format!("Failed to deserialize result: {}", message),
596        )
597        .with_suggestion("Check that the model matches the database schema")
598        .with_suggestion("Ensure data types are compatible")
599    }
600
601    /// Create a general database error.
602    pub fn database(message: impl Into<String>) -> Self {
603        let message = message.into();
604        Self::new(ErrorCode::DatabaseError, message)
605            .with_suggestion("Check the database logs for more details")
606    }
607
608    /// Create an internal error.
609    pub fn internal(message: impl Into<String>) -> Self {
610        let message = message.into();
611        Self::new(ErrorCode::Internal, format!("Internal error: {}", message))
612            .with_help("This is likely a bug in Prax ORM - please report it at https://github.com/pegasusheavy/prax-orm/issues")
613    }
614
615    /// Create an unsupported operation error.
616    pub fn unsupported(message: impl Into<String>) -> Self {
617        let message = message.into();
618        Self::new(ErrorCode::InvalidConfiguration, format!("Unsupported: {}", message))
619            .with_help("This operation is not supported by the current database driver")
620    }
621
622    // ============== Error Checks ==============
623
624    /// Check if this is a not found error.
625    pub fn is_not_found(&self) -> bool {
626        self.code == ErrorCode::RecordNotFound
627    }
628
629    /// Check if this is a constraint violation.
630    pub fn is_constraint_violation(&self) -> bool {
631        matches!(
632            self.code,
633            ErrorCode::UniqueConstraint
634                | ErrorCode::ForeignKeyConstraint
635                | ErrorCode::CheckConstraint
636                | ErrorCode::NotNullConstraint
637        )
638    }
639
640    /// Check if this is a timeout error.
641    pub fn is_timeout(&self) -> bool {
642        matches!(
643            self.code,
644            ErrorCode::QueryTimeout | ErrorCode::ConnectionTimeout
645        )
646    }
647
648    /// Check if this is a connection error.
649    pub fn is_connection_error(&self) -> bool {
650        matches!(
651            self.code,
652            ErrorCode::ConnectionFailed
653                | ErrorCode::PoolExhausted
654                | ErrorCode::ConnectionTimeout
655                | ErrorCode::AuthenticationFailed
656                | ErrorCode::SslError
657        )
658    }
659
660    /// Check if this error is retryable.
661    pub fn is_retryable(&self) -> bool {
662        matches!(
663            self.code,
664            ErrorCode::ConnectionTimeout
665                | ErrorCode::PoolExhausted
666                | ErrorCode::QueryTimeout
667                | ErrorCode::Deadlock
668                | ErrorCode::SerializationFailure
669        )
670    }
671
672    // ============== Display Functions ==============
673
674    /// Get the error code.
675    pub fn error_code(&self) -> &ErrorCode {
676        &self.code
677    }
678
679    /// Get the documentation URL for this error.
680    pub fn docs_url(&self) -> String {
681        self.code.docs_url()
682    }
683
684    /// Display the full error with all context and suggestions.
685    pub fn display_full(&self) -> String {
686        let mut output = String::new();
687
688        // Error header
689        output.push_str(&format!("Error [{}]: {}\n", self.code.code(), self.message));
690
691        // Context
692        if let Some(ref op) = self.context.operation {
693            output.push_str(&format!("  → While: {}\n", op));
694        }
695        if let Some(ref model) = self.context.model {
696            output.push_str(&format!("  → Model: {}\n", model));
697        }
698        if let Some(ref field) = self.context.field {
699            output.push_str(&format!("  → Field: {}\n", field));
700        }
701
702        // SQL (truncated if too long)
703        if let Some(ref sql) = self.context.sql {
704            let sql_display = if sql.len() > 200 {
705                format!("{}...", &sql[..200])
706            } else {
707                sql.clone()
708            };
709            output.push_str(&format!("  → SQL: {}\n", sql_display));
710        }
711
712        // Suggestions
713        if !self.context.suggestions.is_empty() {
714            output.push_str("\nSuggestions:\n");
715            for (i, suggestion) in self.context.suggestions.iter().enumerate() {
716                output.push_str(&format!("  {}. {}\n", i + 1, suggestion.text));
717                if let Some(ref code) = suggestion.code {
718                    output.push_str(&format!(
719                        "     ```\n     {}\n     ```\n",
720                        code.replace('\n', "\n     ")
721                    ));
722                }
723            }
724        }
725
726        // Help
727        if let Some(ref help) = self.context.help {
728            output.push_str(&format!("\nHelp: {}\n", help));
729        }
730
731        // Documentation link
732        output.push_str(&format!("\nMore info: {}\n", self.docs_url()));
733
734        output
735    }
736
737    /// Display error with ANSI colors for terminal output.
738    pub fn display_colored(&self) -> String {
739        let mut output = String::new();
740
741        // Error header (red)
742        output.push_str(&format!(
743            "\x1b[1;31mError [{}]\x1b[0m: \x1b[1m{}\x1b[0m\n",
744            self.code.code(),
745            self.message
746        ));
747
748        // Context (dim)
749        if let Some(ref op) = self.context.operation {
750            output.push_str(&format!("  \x1b[2m→ While:\x1b[0m {}\n", op));
751        }
752        if let Some(ref model) = self.context.model {
753            output.push_str(&format!("  \x1b[2m→ Model:\x1b[0m {}\n", model));
754        }
755        if let Some(ref field) = self.context.field {
756            output.push_str(&format!("  \x1b[2m→ Field:\x1b[0m {}\n", field));
757        }
758
759        // Suggestions (yellow)
760        if !self.context.suggestions.is_empty() {
761            output.push_str("\n\x1b[1;33mSuggestions:\x1b[0m\n");
762            for (i, suggestion) in self.context.suggestions.iter().enumerate() {
763                output.push_str(&format!(
764                    "  \x1b[33m{}.\x1b[0m {}\n",
765                    i + 1,
766                    suggestion.text
767                ));
768                if let Some(ref code) = suggestion.code {
769                    output.push_str(&format!(
770                        "     \x1b[2m```\x1b[0m\n     \x1b[36m{}\x1b[0m\n     \x1b[2m```\x1b[0m\n",
771                        code.replace('\n', "\n     ")
772                    ));
773                }
774            }
775        }
776
777        // Help (cyan)
778        if let Some(ref help) = self.context.help {
779            output.push_str(&format!("\n\x1b[1;36mHelp:\x1b[0m {}\n", help));
780        }
781
782        // Documentation link (blue)
783        output.push_str(&format!(
784            "\n\x1b[2mMore info:\x1b[0m \x1b[4;34m{}\x1b[0m\n",
785            self.docs_url()
786        ));
787
788        output
789    }
790}
791
792/// Extension trait for converting errors to QueryError.
793pub trait IntoQueryError {
794    /// Convert to a QueryError.
795    fn into_query_error(self) -> QueryError;
796}
797
798impl<E: std::error::Error + Send + Sync + 'static> IntoQueryError for E {
799    fn into_query_error(self) -> QueryError {
800        QueryError::internal(self.to_string()).with_source(self)
801    }
802}
803
804/// Helper for creating errors with context.
805#[macro_export]
806macro_rules! query_error {
807    ($code:expr, $msg:expr) => {
808        $crate::error::QueryError::new($code, $msg)
809    };
810    ($code:expr, $msg:expr, $($key:ident = $value:expr),+ $(,)?) => {{
811        let mut err = $crate::error::QueryError::new($code, $msg);
812        $(
813            err = err.$key($value);
814        )+
815        err
816    }};
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822
823    #[test]
824    fn test_error_code_format() {
825        assert_eq!(ErrorCode::RecordNotFound.code(), "P1001");
826        assert_eq!(ErrorCode::UniqueConstraint.code(), "P2001");
827        assert_eq!(ErrorCode::ConnectionFailed.code(), "P3001");
828    }
829
830    #[test]
831    fn test_not_found_error() {
832        let err = QueryError::not_found("User");
833        assert!(err.is_not_found());
834        assert!(err.message.contains("User"));
835        assert!(!err.context.suggestions.is_empty());
836    }
837
838    #[test]
839    fn test_unique_violation_error() {
840        let err = QueryError::unique_violation("User", "email");
841        assert!(err.is_constraint_violation());
842        assert_eq!(err.context.model, Some("User".to_string()));
843        assert_eq!(err.context.field, Some("email".to_string()));
844    }
845
846    #[test]
847    fn test_timeout_error() {
848        let err = QueryError::timeout(5000);
849        assert!(err.is_timeout());
850        assert!(err.message.contains("5000"));
851    }
852
853    #[test]
854    fn test_error_with_context() {
855        let err = QueryError::not_found("User")
856            .with_context("Finding user by email")
857            .with_suggestion("Use a different query method");
858
859        assert_eq!(
860            err.context.operation,
861            Some("Finding user by email".to_string())
862        );
863        assert!(err.context.suggestions.len() >= 2); // Original + new one
864    }
865
866    #[test]
867    fn test_retryable_errors() {
868        assert!(QueryError::timeout(1000).is_retryable());
869        assert!(QueryError::deadlock().is_retryable());
870        assert!(QueryError::pool_exhausted(10).is_retryable());
871        assert!(!QueryError::not_found("User").is_retryable());
872    }
873
874    #[test]
875    fn test_connection_errors() {
876        assert!(QueryError::connection("failed").is_connection_error());
877        assert!(QueryError::authentication_failed("bad password").is_connection_error());
878        assert!(QueryError::pool_exhausted(10).is_connection_error());
879    }
880
881    #[test]
882    fn test_display_full() {
883        let err = QueryError::unique_violation("User", "email").with_context("Creating new user");
884
885        let output = err.display_full();
886        assert!(output.contains("P2001"));
887        assert!(output.contains("User"));
888        assert!(output.contains("email"));
889        assert!(output.contains("Suggestions"));
890    }
891
892    #[test]
893    fn test_docs_url() {
894        let err = QueryError::not_found("User");
895        assert!(err.docs_url().contains("P1001"));
896    }
897
898    #[test]
899    fn test_error_macro() {
900        let err = query_error!(
901            ErrorCode::InvalidParameter,
902            "Invalid email format",
903            with_field = "email",
904            with_suggestion = "Use a valid email address"
905        );
906
907        assert_eq!(err.code, ErrorCode::InvalidParameter);
908        assert_eq!(err.context.field, Some("email".to_string()));
909    }
910
911    #[test]
912    fn test_suggestion_with_code() {
913        let err = QueryError::not_found("User")
914            .with_code_suggestion("Try this instead", "client.user().find_first()");
915
916        let suggestion = err.context.suggestions.last().unwrap();
917        assert!(suggestion.code.is_some());
918    }
919}