1use std::fmt;
61use thiserror::Error;
62
63pub type QueryResult<T> = Result<T, QueryError>;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum ErrorCode {
69 RecordNotFound = 1001,
72 NotUnique = 1002,
74 InvalidFilter = 1003,
76 InvalidSelect = 1004,
78 RequiredFieldMissing = 1005,
80
81 UniqueConstraint = 2001,
84 ForeignKeyConstraint = 2002,
86 CheckConstraint = 2003,
88 NotNullConstraint = 2004,
90
91 ConnectionFailed = 3001,
94 PoolExhausted = 3002,
96 ConnectionTimeout = 3003,
98 AuthenticationFailed = 3004,
100 SslError = 3005,
102
103 TransactionFailed = 4001,
106 Deadlock = 4002,
108 SerializationFailure = 4003,
110 TransactionClosed = 4004,
112
113 QueryTimeout = 5001,
116 SqlSyntax = 5002,
118 InvalidParameter = 5003,
120 QueryTooComplex = 5004,
122 DatabaseError = 5005,
124
125 InvalidDataType = 6001,
128 SerializationError = 6002,
130 DeserializationError = 6003,
132 DataTruncation = 6004,
134
135 InvalidConfiguration = 7001,
138 MissingConfiguration = 7002,
140 InvalidConnectionString = 7003,
142
143 Internal = 9001,
146 Unknown = 9999,
148}
149
150impl ErrorCode {
151 pub fn code(&self) -> String {
153 format!("P{}", *self as u16)
154 }
155
156 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 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#[derive(Debug, Clone)]
208pub struct Suggestion {
209 pub text: String,
211 pub code: Option<String>,
213}
214
215impl Suggestion {
216 pub fn new(text: impl Into<String>) -> Self {
218 Self {
219 text: text.into(),
220 code: None,
221 }
222 }
223
224 pub fn with_code(mut self, code: impl Into<String>) -> Self {
226 self.code = Some(code.into());
227 self
228 }
229}
230
231#[derive(Debug, Clone, Default)]
233pub struct ErrorContext {
234 pub operation: Option<String>,
236 pub model: Option<String>,
238 pub field: Option<String>,
240 pub sql: Option<String>,
242 pub suggestions: Vec<Suggestion>,
244 pub help: Option<String>,
246 pub related: Vec<String>,
248}
249
250impl ErrorContext {
251 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn operation(mut self, op: impl Into<String>) -> Self {
258 self.operation = Some(op.into());
259 self
260 }
261
262 pub fn model(mut self, model: impl Into<String>) -> Self {
264 self.model = Some(model.into());
265 self
266 }
267
268 pub fn field(mut self, field: impl Into<String>) -> Self {
270 self.field = Some(field.into());
271 self
272 }
273
274 pub fn sql(mut self, sql: impl Into<String>) -> Self {
276 self.sql = Some(sql.into());
277 self
278 }
279
280 pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
282 self.suggestions.push(suggestion);
283 self
284 }
285
286 pub fn suggest(mut self, text: impl Into<String>) -> Self {
288 self.suggestions.push(Suggestion::new(text));
289 self
290 }
291
292 pub fn help(mut self, help: impl Into<String>) -> Self {
294 self.help = Some(help.into());
295 self
296 }
297}
298
299#[derive(Error, Debug)]
301pub struct QueryError {
302 pub code: ErrorCode,
304 pub message: String,
306 pub context: ErrorContext,
308 #[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 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 pub fn with_context(mut self, operation: impl Into<String>) -> Self {
332 self.context.operation = Some(operation.into());
333 self
334 }
335
336 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
338 self.context.suggestions.push(Suggestion::new(suggestion));
339 self
340 }
341
342 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 pub fn with_help(mut self, help: impl Into<String>) -> Self {
356 self.context.help = Some(help.into());
357 self
358 }
359
360 pub fn with_model(mut self, model: impl Into<String>) -> Self {
362 self.context.model = Some(model.into());
363 self
364 }
365
366 pub fn with_field(mut self, field: impl Into<String>) -> Self {
368 self.context.field = Some(field.into());
369 self
370 }
371
372 pub fn with_sql(mut self, sql: impl Into<String>) -> Self {
374 self.context.sql = Some(sql.into());
375 self
376 }
377
378 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn serialization(message: impl Into<String>) -> Self {
587 Self::new(ErrorCode::SerializationError, message.into())
588 }
589
590 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 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 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 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 pub fn is_not_found(&self) -> bool {
626 self.code == ErrorCode::RecordNotFound
627 }
628
629 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 pub fn is_timeout(&self) -> bool {
642 matches!(
643 self.code,
644 ErrorCode::QueryTimeout | ErrorCode::ConnectionTimeout
645 )
646 }
647
648 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 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 pub fn error_code(&self) -> &ErrorCode {
676 &self.code
677 }
678
679 pub fn docs_url(&self) -> String {
681 self.code.docs_url()
682 }
683
684 pub fn display_full(&self) -> String {
686 let mut output = String::new();
687
688 output.push_str(&format!("Error [{}]: {}\n", self.code.code(), self.message));
690
691 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 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 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 if let Some(ref help) = self.context.help {
728 output.push_str(&format!("\nHelp: {}\n", help));
729 }
730
731 output.push_str(&format!("\nMore info: {}\n", self.docs_url()));
733
734 output
735 }
736
737 pub fn display_colored(&self) -> String {
739 let mut output = String::new();
740
741 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 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 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 if let Some(ref help) = self.context.help {
779 output.push_str(&format!("\n\x1b[1;36mHelp:\x1b[0m {}\n", help));
780 }
781
782 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
792pub trait IntoQueryError {
794 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#[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); }
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}