1pub mod codes;
18pub mod response;
19
20pub use response::ErrorResponse;
21
22use thiserror::Error;
23
24#[derive(Debug, Error)]
28pub enum Error {
29 #[error("Database connection failed: {0}")]
33 DbConnection(String),
34
35 #[error("Unsupported PostgreSQL version: {major}.{minor} (minimum: 12.0)")]
36 UnsupportedPgVersion { major: u32, minor: u32 },
37
38 #[error("Invalid configuration: {message}")]
39 InvalidConfig { message: String },
40
41 #[error("Database connection retry timeout")]
42 ConnectionRetryTimeout,
43
44 #[error("Invalid query parameter '{param}': {message}")]
48 InvalidQueryParam { param: String, message: String },
49
50 #[error("Parse error in {location}: {message}")]
51 ParseError { location: String, message: String },
52
53 #[error("Invalid range: {0}")]
54 InvalidRange(String),
55
56 #[error("Invalid content-type: {0}")]
57 InvalidContentType(String),
58
59 #[error("Invalid preference: {0}")]
60 InvalidPreference(String),
61
62 #[error("Invalid filter operator '{op}' on column '{column}'")]
63 InvalidFilterOperator { column: String, op: String },
64
65 #[error("Ambiguous embedding: multiple relationships found for '{0}'")]
66 AmbiguousEmbedding(String),
67
68 #[error("Invalid embedding: {0}")]
69 InvalidEmbedding(String),
70
71 #[error("Invalid request body: {0}")]
72 InvalidBody(String),
73
74 #[error("Schema not found: {0}")]
75 SchemaNotFound(String),
76
77 #[error("Invalid column in spread relationship: {0}")]
78 InvalidSpreadColumn(String),
79
80 #[error("Invalid content type for media handler: {0}")]
81 InvalidMediaHandler(String),
82
83 #[error("Media types mismatch: {0}")]
84 MediaTypeMismatch(String),
85
86 #[error("URI too long: {0}")]
87 UriTooLong(String),
88
89 #[error("Invalid aggregate: {0}")]
90 InvalidAggregate(String),
91
92 #[error(
93 "response.headers GUC must be a JSON array composed of objects with a single key and a string value"
94 )]
95 GucHeadersError,
96
97 #[error("response.status GUC must be a valid status code")]
98 GucStatusError,
99
100 #[error("PUT with limit/offset querystring parameters is not allowed")]
101 PutLimitNotAllowed,
102
103 #[error("Payload values do not match URL in primary key column(s)")]
104 PutMatchingPkError,
105
106 #[error("Cannot coerce the result to a single JSON object")]
107 SingularityError { count: i64 },
108
109 #[error("Unsupported HTTP method: {0}")]
110 UnsupportedMethod(String),
111
112 #[error("A related order on '{target}' is not possible")]
113 RelatedOrderNotToOne { origin: String, target: String },
114
115 #[error("Bad operator on the '{target}' embedded resource")]
116 UnacceptableFilter { target: String },
117
118 #[error("Could not parse JSON in the \"RAISE SQLSTATE 'DBRST'\" error: {0}")]
119 DbrstParseError(String),
120
121 #[error("Invalid preferences given with handling=strict: {0}")]
122 InvalidPreferencesStrict(String),
123
124 #[error("Use of aggregate functions is not allowed")]
125 AggregatesNotAllowed,
126
127 #[error("Query result exceeds max-affected preference constraint: {count} rows")]
128 MaxAffectedViolation { count: i64 },
129
130 #[error("Invalid path specified in request URL")]
131 InvalidResourcePath,
132
133 #[error("Root endpoint metadata is disabled")]
134 OpenApiDisabled,
135
136 #[error("Feature not implemented: {0}")]
137 NotImplemented(String),
138
139 #[error(
140 "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
141 )]
142 MaxAffectedRpcViolation,
143
144 #[error("Table not found: {name}")]
148 TableNotFound {
149 name: String,
150 suggestion: Option<String>,
151 },
152
153 #[error("Column '{column}' not found in table '{table}'")]
154 ColumnNotFound { table: String, column: String },
155
156 #[error("Function not found: {name}")]
157 FunctionNotFound { name: String },
158
159 #[error("Relationship not found between '{from_table}' and '{to_table}'")]
160 RelationshipNotFound {
161 from_table: String,
162 to_table: String,
163 },
164
165 #[error("Schema cache not ready")]
166 SchemaCacheNotReady,
167
168 #[error(
169 "Ambiguous relationship: multiple relationships found between '{from_table}' and '{to_table}'"
170 )]
171 AmbiguousRelationship {
172 from_table: String,
173 to_table: String,
174 },
175
176 #[error("Ambiguous function: multiple function overloads found for '{name}'")]
177 AmbiguousFunction { name: String },
178
179 #[error("{0}")]
183 JwtAuth(#[from] crate::auth::error::JwtError),
184
185 #[error("JWT error: {0}")]
186 Jwt(String),
187
188 #[error("No anonymous role configured")]
189 NoAnonRole,
190
191 #[error("Permission denied for role '{role}'")]
192 PermissionDenied { role: String },
193
194 #[error("Table '{table}' is not insertable")]
198 NotInsertable { table: String },
199
200 #[error("Table '{table}' is not updatable")]
201 NotUpdatable { table: String },
202
203 #[error("Table '{table}' is not deletable")]
204 NotDeletable { table: String },
205
206 #[error("Single object expected but multiple rows returned")]
207 SingleObjectExpected,
208
209 #[error("Missing required payload")]
210 MissingPayload,
211
212 #[error("Invalid payload: {0}")]
213 InvalidPayload(String),
214
215 #[error("No primary key found for table '{table}'")]
216 NoPrimaryKey { table: String },
217
218 #[error("PUT requires all primary key columns")]
219 PutIncomplete,
220
221 #[error("Database error: {message}")]
225 Database {
226 code: Option<String>,
227 message: String,
228 detail: Option<String>,
229 hint: Option<String>,
230 },
231
232 #[error("Foreign key violation: {0}")]
233 ForeignKeyViolation(String),
234
235 #[error("Unique constraint violation: {0}")]
236 UniqueViolation(String),
237
238 #[error("Check constraint violation: {0}")]
239 CheckViolation(String),
240
241 #[error("Not null violation: {0}")]
242 NotNullViolation(String),
243
244 #[error("Exclusion violation: {0}")]
245 ExclusionViolation(String),
246
247 #[error("Row count limit exceeded: {count} rows affected, max is {max}")]
248 MaxRowsExceeded { count: i64, max: i64 },
249
250 #[error("Raised exception: {message}")]
251 RaisedException {
252 message: String,
253 status: Option<u16>,
254 },
255
256 #[error("PostgREST raise: {message}")]
257 DbrstRaise { message: String, status: u16 },
258
259 #[error("Internal error: {0}")]
263 Internal(String),
264}
265
266impl Error {
267 pub fn code(&self) -> &'static str {
269 match self {
270 Error::DbConnection(_) => codes::config::DB_CONNECTION,
272 Error::UnsupportedPgVersion { .. } => codes::config::UNSUPPORTED_PG_VERSION,
273 Error::InvalidConfig { .. } => codes::config::INVALID_CONFIG,
274 Error::ConnectionRetryTimeout => codes::config::CONNECTION_RETRY_TIMEOUT,
275
276 Error::InvalidQueryParam { .. } => codes::request::INVALID_QUERY_PARAM,
278 Error::ParseError { .. } => codes::request::PARSE_ERROR,
279 Error::InvalidRange(_) => codes::request::INVALID_RANGE,
280 Error::InvalidContentType(_) => codes::request::INVALID_CONTENT_TYPE,
281 Error::InvalidPreference(_) => codes::request::INVALID_PREFERENCE,
282 Error::InvalidFilterOperator { .. } => codes::request::INVALID_FILTER_OPERATOR,
283 Error::SchemaNotFound(_) => codes::request::SCHEMA_NOT_FOUND,
284 Error::InvalidSpreadColumn(_) => codes::request::INVALID_SPREAD_COLUMN,
285 Error::AmbiguousEmbedding(_) => codes::request::AMBIGUOUS_EMBEDDING,
286 Error::InvalidEmbedding(_) => codes::request::INVALID_EMBEDDING,
287 Error::InvalidBody(_) => codes::request::INVALID_BODY,
288 Error::InvalidMediaHandler(_) => codes::request::INVALID_MEDIA_HANDLER,
289 Error::MediaTypeMismatch(_) => codes::request::MEDIA_TYPE_MISMATCH,
290 Error::UriTooLong(_) => codes::request::URI_TOO_LONG,
291 Error::InvalidAggregate(_) => codes::request::INVALID_AGGREGATE,
292 Error::GucHeadersError => codes::request::GUC_HEADERS_ERROR,
293 Error::GucStatusError => codes::request::GUC_STATUS_ERROR,
294 Error::PutLimitNotAllowed => codes::request::PUT_LIMIT_NOT_ALLOWED,
295 Error::PutMatchingPkError => codes::request::PUT_MATCHING_PK_ERROR,
296 Error::SingularityError { .. } => codes::request::SINGULARITY_ERROR,
297 Error::UnsupportedMethod(_) => codes::request::UNSUPPORTED_METHOD,
298 Error::RelatedOrderNotToOne { .. } => codes::request::RELATED_ORDER_NOT_TO_ONE,
299 Error::UnacceptableFilter { .. } => codes::request::UNACCEPTABLE_FILTER,
300 Error::DbrstParseError(_) => codes::request::DBRST_PARSE_ERROR,
301 Error::InvalidPreferencesStrict(_) => codes::request::INVALID_PREFERENCES,
302 Error::AggregatesNotAllowed => codes::request::AGGREGATES_NOT_ALLOWED,
303 Error::MaxAffectedViolation { .. } => codes::request::MAX_AFFECTED_VIOLATION,
304 Error::InvalidResourcePath => codes::request::INVALID_RESOURCE_PATH,
305 Error::OpenApiDisabled => codes::request::OPENAPI_DISABLED,
306 Error::NotImplemented(_) => codes::request::NOT_IMPLEMENTED,
307 Error::MaxAffectedRpcViolation => codes::request::MAX_AFFECTED_RPC_VIOLATION,
308
309 Error::TableNotFound { .. } => codes::schema::TABLE_NOT_FOUND,
311 Error::ColumnNotFound { .. } => codes::schema::COLUMN_NOT_FOUND,
312 Error::FunctionNotFound { .. } => codes::schema::FUNCTION_NOT_FOUND,
313 Error::RelationshipNotFound { .. } => codes::schema::RELATIONSHIP_NOT_FOUND,
314 Error::AmbiguousRelationship { .. } => codes::schema::AMBIGUOUS_RELATIONSHIP,
315 Error::AmbiguousFunction { .. } => codes::schema::AMBIGUOUS_FUNCTION,
316 Error::SchemaCacheNotReady => codes::schema::SCHEMA_CACHE_NOT_READY,
317
318 Error::JwtAuth(e) => e.code(),
320 Error::Jwt(_) => codes::auth::JWT_ERROR,
321 Error::NoAnonRole => codes::auth::NO_ANON_ROLE,
322 Error::PermissionDenied { .. } => codes::auth::CLAIMS_ERROR,
323
324 Error::NotInsertable { .. } => codes::action::NOT_INSERTABLE,
326 Error::NotUpdatable { .. } => codes::action::NOT_UPDATABLE,
327 Error::NotDeletable { .. } => codes::action::NOT_DELETABLE,
328 Error::SingleObjectExpected => codes::action::SINGLE_OBJECT_EXPECTED,
329 Error::MissingPayload => codes::action::MISSING_PAYLOAD,
330 Error::InvalidPayload(_) => codes::action::INVALID_PAYLOAD,
331 Error::NoPrimaryKey { .. } => codes::action::NO_PRIMARY_KEY,
332 Error::PutIncomplete => codes::action::PUT_INCOMPLETE,
333
334 Error::Database { .. } => codes::database::DB_ERROR,
336 Error::ForeignKeyViolation(_) => codes::database::FK_VIOLATION,
337 Error::UniqueViolation(_) => codes::database::UNIQUE_VIOLATION,
338 Error::CheckViolation(_) => codes::database::CHECK_VIOLATION,
339 Error::NotNullViolation(_) => codes::database::NOT_NULL_VIOLATION,
340 Error::ExclusionViolation(_) => codes::database::EXCLUSION_VIOLATION,
341 Error::MaxRowsExceeded { .. } => codes::database::MAX_ROWS_EXCEEDED,
342 Error::RaisedException { .. } => codes::database::RAISED_EXCEPTION,
343 Error::DbrstRaise { .. } => codes::database::DBRST_RAISE,
344
345 Error::Internal(_) => codes::internal::INTERNAL_ERROR,
347 }
348 }
349
350 pub fn status(&self) -> http::StatusCode {
352 use http::StatusCode;
353
354 match self {
355 Error::DbConnection(_)
357 | Error::UnsupportedPgVersion { .. }
358 | Error::InvalidConfig { .. }
359 | Error::ConnectionRetryTimeout
360 | Error::SchemaCacheNotReady => StatusCode::SERVICE_UNAVAILABLE,
361
362 Error::InvalidQueryParam { .. }
364 | Error::ParseError { .. }
365 | Error::InvalidRange(_)
366 | Error::InvalidContentType(_)
367 | Error::InvalidPreference(_)
368 | Error::InvalidFilterOperator { .. }
369 | Error::SchemaNotFound(_)
370 | Error::InvalidSpreadColumn(_)
371 | Error::AmbiguousEmbedding(_)
372 | Error::InvalidEmbedding(_)
373 | Error::InvalidBody(_)
374 | Error::InvalidPayload(_)
375 | Error::MissingPayload
376 | Error::PutIncomplete
377 | Error::InvalidMediaHandler(_)
378 | Error::MediaTypeMismatch(_)
379 | Error::UriTooLong(_)
380 | Error::InvalidAggregate(_)
381 | Error::NotNullViolation(_)
382 | Error::MaxRowsExceeded { .. }
383 | Error::GucHeadersError
384 | Error::GucStatusError
385 | Error::PutLimitNotAllowed
386 | Error::PutMatchingPkError
387 | Error::SingularityError { .. }
388 | Error::UnsupportedMethod(_)
389 | Error::RelatedOrderNotToOne { .. }
390 | Error::UnacceptableFilter { .. }
391 | Error::DbrstParseError(_)
392 | Error::InvalidPreferencesStrict(_)
393 | Error::AggregatesNotAllowed
394 | Error::MaxAffectedViolation { .. }
395 | Error::NotImplemented(_)
396 | Error::MaxAffectedRpcViolation => StatusCode::BAD_REQUEST,
397
398 Error::TableNotFound { .. }
400 | Error::ColumnNotFound { .. }
401 | Error::FunctionNotFound { .. }
402 | Error::RelationshipNotFound { .. }
403 | Error::InvalidResourcePath
404 | Error::OpenApiDisabled => StatusCode::NOT_FOUND,
405
406 Error::AmbiguousRelationship { .. } | Error::AmbiguousFunction { .. } => {
408 StatusCode::MULTIPLE_CHOICES
409 }
410
411 Error::JwtAuth(e) => e.status(),
413 Error::Jwt(_) | Error::NoAnonRole => StatusCode::UNAUTHORIZED,
414 Error::PermissionDenied { .. } => StatusCode::FORBIDDEN,
415
416 Error::NotInsertable { .. }
418 | Error::NotUpdatable { .. }
419 | Error::NotDeletable { .. } => StatusCode::METHOD_NOT_ALLOWED,
420
421 Error::NoPrimaryKey { .. }
423 | Error::UniqueViolation(_)
424 | Error::ForeignKeyViolation(_)
425 | Error::CheckViolation(_)
426 | Error::ExclusionViolation(_) => StatusCode::CONFLICT,
427
428 Error::SingleObjectExpected => StatusCode::NOT_ACCEPTABLE,
430
431 Error::RaisedException { status, .. } => status
433 .and_then(|s| StatusCode::from_u16(s).ok())
434 .unwrap_or(StatusCode::BAD_REQUEST),
435
436 Error::DbrstRaise { status, .. } => {
438 StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_REQUEST)
439 }
440
441 Error::Database { .. } | Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
443 }
444 }
445
446 pub fn details_and_hint(&self) -> (Option<String>, Option<String>) {
448 match self {
449 Error::TableNotFound { suggestion, .. } => {
450 (None, suggestion.as_ref().map(|s| format!("Did you mean '{}'?", s)))
451 }
452 Error::ColumnNotFound { table, column } => (
453 Some(format!("Column '{}' does not exist in table '{}'", column, table)),
454 Some("Check the table schema for available columns".to_string()),
455 ),
456 Error::FunctionNotFound { name } => (
457 Some(format!("Function '{}' does not exist", name)),
458 Some("Check the schema for available functions".to_string()),
459 ),
460 Error::RelationshipNotFound { from_table, to_table } => (
461 Some(format!("No relationship found between '{}' and '{}'", from_table, to_table)),
462 Some("Ensure foreign key constraints exist between these tables".to_string()),
463 ),
464 Error::Database { detail, hint, .. } => (detail.clone(), hint.clone()),
465 Error::InvalidQueryParam { message, .. } => (Some(message.clone()), None),
466 Error::ParseError { location, message } => {
467 (Some(format!("At {}: {}", location, message)), None)
468 }
469 Error::InvalidFilterOperator { column, op } => (
470 Some(format!("Operator '{}' is not valid for column '{}'", op, column)),
471 Some("Use valid operators: eq, neq, gt, gte, lt, lte, like, ilike, is, in, cs, cd, ov, sl, sr, nxr, nxl, adj".to_string()),
472 ),
473 Error::AmbiguousEmbedding(rel) => (
474 None,
475 Some(format!(
476 "Use the hint parameter to disambiguate: ?select={}!hint_name(*)",
477 rel
478 )),
479 ),
480 Error::InvalidBody(msg) => (
481 Some(msg.clone()),
482 Some("Ensure the request body is valid JSON".to_string()),
483 ),
484 Error::InvalidPayload(msg) => (
485 Some(msg.clone()),
486 Some("Check the payload format and required fields".to_string()),
487 ),
488 Error::MaxRowsExceeded { count, max } => (
489 Some(format!("Affected {} rows, but maximum allowed is {}", count, max)),
490 Some("Reduce the scope of your request or increase the limit".to_string()),
491 ),
492 Error::NotInsertable { table } => (
493 Some(format!("Table '{}' does not allow INSERT operations", table)),
494 Some("Check table permissions and RLS policies".to_string()),
495 ),
496 Error::NotUpdatable { table } => (
497 Some(format!("Table '{}' does not allow UPDATE operations", table)),
498 Some("Check table permissions and RLS policies".to_string()),
499 ),
500 Error::NotDeletable { table } => (
501 Some(format!("Table '{}' does not allow DELETE operations", table)),
502 Some("Check table permissions and RLS policies".to_string()),
503 ),
504 Error::UniqueViolation(msg) => (
505 Some(msg.clone()),
506 Some("The value violates a unique constraint. Use a different value or update the existing record".to_string()),
507 ),
508 Error::ForeignKeyViolation(msg) => (
509 Some(msg.clone()),
510 Some("The value references a non-existent record. Ensure the referenced record exists".to_string()),
511 ),
512 Error::CheckViolation(msg) => (
513 Some(msg.clone()),
514 Some("The value violates a check constraint. Check the constraint requirements".to_string()),
515 ),
516 Error::NotNullViolation(msg) => (
517 Some(msg.clone()),
518 Some("A required field is missing or null. Provide a value for all required fields".to_string()),
519 ),
520 Error::ExclusionViolation(msg) => (
521 Some(msg.clone()),
522 Some("The value violates an exclusion constraint".to_string()),
523 ),
524 Error::RaisedException { message, .. } => (
525 Some(message.clone()),
526 None,
527 ),
528 Error::DbrstRaise { message, .. } => (
529 Some(message.clone()),
530 None,
531 ),
532 Error::SingularityError { count } => (
533 Some(format!("The result contains {} rows", count)),
534 None,
535 ),
536 Error::MaxAffectedViolation { count } => (
537 Some(format!("The query affects {} rows", count)),
538 None,
539 ),
540 Error::GucHeadersError => (
541 Some("response.headers GUC must be a JSON array composed of objects with a single key and a string value".to_string()),
542 None,
543 ),
544 Error::GucStatusError => (
545 Some("response.status GUC must be a valid status code".to_string()),
546 None,
547 ),
548 Error::AmbiguousRelationship { from_table, to_table } => (
549 Some(format!("Multiple relationships found between '{}' and '{}'", from_table, to_table)),
550 Some("Use the hint parameter to disambiguate".to_string()),
551 ),
552 Error::AmbiguousFunction { name } => (
553 Some(format!("Multiple function overloads found for '{}'", name)),
554 Some("Try renaming the parameters or the function itself in the database so function overloading can be resolved".to_string()),
555 ),
556 _ => (None, None),
557 }
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn test_error_codes() {
567 let err = Error::TableNotFound {
568 name: "users".to_string(),
569 suggestion: None,
570 };
571 assert_eq!(err.code(), "DBRST205"); assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
573 }
574
575 #[test]
576 fn test_error_status_mapping() {
577 assert_eq!(
578 Error::DbConnection("test".to_string()).status(),
579 http::StatusCode::SERVICE_UNAVAILABLE
580 );
581 assert_eq!(
582 Error::InvalidRange("test".to_string()).status(),
583 http::StatusCode::BAD_REQUEST
584 );
585 assert_eq!(
586 Error::Jwt("invalid".to_string()).status(),
587 http::StatusCode::UNAUTHORIZED
588 );
589 assert_eq!(
590 Error::PermissionDenied {
591 role: "test".to_string()
592 }
593 .status(),
594 http::StatusCode::FORBIDDEN
595 );
596 }
597
598 #[test]
599 fn test_suggestion_hint() {
600 let err = Error::TableNotFound {
601 name: "usrs".to_string(),
602 suggestion: Some("users".to_string()),
603 };
604 let (_, hint) = err.details_and_hint();
605 assert!(hint.unwrap().contains("users"));
606 }
607
608 #[test]
609 fn test_new_error_codes() {
610 assert_eq!(
612 Error::ConnectionRetryTimeout.code(),
613 codes::config::CONNECTION_RETRY_TIMEOUT
614 );
615 assert_eq!(
616 Error::ConnectionRetryTimeout.status(),
617 http::StatusCode::SERVICE_UNAVAILABLE
618 );
619
620 assert_eq!(
622 Error::SchemaNotFound("test".to_string()).code(),
623 codes::request::SCHEMA_NOT_FOUND
624 );
625 assert_eq!(
626 Error::SchemaNotFound("test".to_string()).status(),
627 http::StatusCode::BAD_REQUEST
628 );
629
630 assert_eq!(
632 Error::InvalidSpreadColumn("col".to_string()).code(),
633 codes::request::INVALID_SPREAD_COLUMN
634 );
635
636 assert_eq!(
638 Error::InvalidMediaHandler("handler".to_string()).code(),
639 codes::request::INVALID_MEDIA_HANDLER
640 );
641
642 assert_eq!(
644 Error::MediaTypeMismatch("mismatch".to_string()).code(),
645 codes::request::MEDIA_TYPE_MISMATCH
646 );
647
648 assert_eq!(
650 Error::UriTooLong("uri".to_string()).code(),
651 codes::request::URI_TOO_LONG
652 );
653
654 assert_eq!(
656 Error::InvalidAggregate("agg".to_string()).code(),
657 codes::request::INVALID_AGGREGATE
658 );
659
660 assert_eq!(
662 Error::NotNullViolation("msg".to_string()).code(),
663 codes::database::NOT_NULL_VIOLATION
664 );
665 assert_eq!(
666 Error::NotNullViolation("msg".to_string()).status(),
667 http::StatusCode::BAD_REQUEST
668 );
669
670 assert_eq!(
672 Error::ExclusionViolation("msg".to_string()).code(),
673 codes::database::EXCLUSION_VIOLATION
674 );
675 assert_eq!(
676 Error::ExclusionViolation("msg".to_string()).status(),
677 http::StatusCode::CONFLICT
678 );
679
680 assert_eq!(
682 Error::RaisedException {
683 message: "test".to_string(),
684 status: None
685 }
686 .code(),
687 codes::database::RAISED_EXCEPTION
688 );
689 assert_eq!(
690 Error::RaisedException {
691 message: "test".to_string(),
692 status: Some(202)
693 }
694 .status(),
695 http::StatusCode::ACCEPTED
696 );
697
698 assert_eq!(
700 Error::DbrstRaise {
701 message: "test".to_string(),
702 status: 418
703 }
704 .code(),
705 codes::database::DBRST_RAISE
706 );
707 assert_eq!(
708 Error::DbrstRaise {
709 message: "test".to_string(),
710 status: 418
711 }
712 .status(),
713 http::StatusCode::IM_A_TEAPOT
714 );
715 }
716
717 #[test]
718 fn test_error_serialization() {
719 let errs = vec![
721 Error::ConnectionRetryTimeout,
722 Error::SchemaNotFound("test".to_string()),
723 Error::InvalidSpreadColumn("col".to_string()),
724 Error::InvalidMediaHandler("handler".to_string()),
725 Error::MediaTypeMismatch("mismatch".to_string()),
726 Error::UriTooLong("uri".to_string()),
727 Error::InvalidAggregate("agg".to_string()),
728 Error::NotNullViolation("msg".to_string()),
729 Error::ExclusionViolation("msg".to_string()),
730 Error::RaisedException {
731 message: "test".to_string(),
732 status: None,
733 },
734 Error::DbrstRaise {
735 message: "test".to_string(),
736 status: 400,
737 },
738 ];
739
740 for err in errs {
741 let response = crate::error::ErrorResponse::from(&err);
742 let json = serde_json::to_string(&response).unwrap();
743 assert!(json.contains(err.code()));
744 assert!(json.contains("code"));
745 assert!(json.contains("message"));
746 }
747 }
748
749 #[test]
750 fn test_all_error_codes_have_status() {
751 let errs = vec![
753 Error::DbConnection("test".to_string()),
754 Error::UnsupportedPgVersion {
755 major: 11,
756 minor: 0,
757 },
758 Error::InvalidConfig {
759 message: "test".to_string(),
760 },
761 Error::ConnectionRetryTimeout,
762 Error::InvalidQueryParam {
763 param: "test".to_string(),
764 message: "test".to_string(),
765 },
766 Error::ParseError {
767 location: "test".to_string(),
768 message: "test".to_string(),
769 },
770 Error::InvalidRange("test".to_string()),
771 Error::InvalidContentType("test".to_string()),
772 Error::InvalidPreference("test".to_string()),
773 Error::InvalidFilterOperator {
774 column: "test".to_string(),
775 op: "test".to_string(),
776 },
777 Error::SchemaNotFound("test".to_string()),
778 Error::InvalidSpreadColumn("test".to_string()),
779 Error::AmbiguousEmbedding("test".to_string()),
780 Error::InvalidEmbedding("test".to_string()),
781 Error::InvalidBody("test".to_string()),
782 Error::InvalidMediaHandler("test".to_string()),
783 Error::MediaTypeMismatch("test".to_string()),
784 Error::UriTooLong("test".to_string()),
785 Error::InvalidAggregate("test".to_string()),
786 Error::TableNotFound {
787 name: "test".to_string(),
788 suggestion: None,
789 },
790 Error::ColumnNotFound {
791 table: "test".to_string(),
792 column: "test".to_string(),
793 },
794 Error::FunctionNotFound {
795 name: "test".to_string(),
796 },
797 Error::RelationshipNotFound {
798 from_table: "test".to_string(),
799 to_table: "test".to_string(),
800 },
801 Error::SchemaCacheNotReady,
802 Error::Jwt("test".to_string()),
803 Error::NoAnonRole,
804 Error::PermissionDenied {
805 role: "test".to_string(),
806 },
807 Error::NotInsertable {
808 table: "test".to_string(),
809 },
810 Error::NotUpdatable {
811 table: "test".to_string(),
812 },
813 Error::NotDeletable {
814 table: "test".to_string(),
815 },
816 Error::SingleObjectExpected,
817 Error::MissingPayload,
818 Error::InvalidPayload("test".to_string()),
819 Error::NoPrimaryKey {
820 table: "test".to_string(),
821 },
822 Error::PutIncomplete,
823 Error::Database {
824 code: None,
825 message: "test".to_string(),
826 detail: None,
827 hint: None,
828 },
829 Error::ForeignKeyViolation("test".to_string()),
830 Error::UniqueViolation("test".to_string()),
831 Error::CheckViolation("test".to_string()),
832 Error::NotNullViolation("test".to_string()),
833 Error::ExclusionViolation("test".to_string()),
834 Error::MaxRowsExceeded { count: 10, max: 5 },
835 Error::RaisedException {
836 message: "test".to_string(),
837 status: None,
838 },
839 Error::DbrstRaise {
840 message: "test".to_string(),
841 status: 400,
842 },
843 Error::Internal("test".to_string()),
844 ];
845
846 for err in errs {
847 let status = err.status();
848 assert!(status.as_u16() >= 400 || status.as_u16() == 200 || status.as_u16() == 201);
850 }
851 }
852
853 #[test]
854 fn test_details_and_hints() {
855 let err = Error::ColumnNotFound {
857 table: "users".to_string(),
858 column: "email".to_string(),
859 };
860 let (details, hint) = err.details_and_hint();
861 assert!(details.is_some());
862 assert!(hint.is_some());
863 assert!(details.unwrap().contains("email"));
864 assert!(hint.unwrap().contains("schema"));
865
866 let err = Error::MaxRowsExceeded {
867 count: 100,
868 max: 50,
869 };
870 let (details, hint) = err.details_and_hint();
871 assert!(details.is_some());
872 assert!(hint.is_some());
873 let details_str = details.unwrap();
874 assert!(details_str.contains("100"));
875 assert!(details_str.contains("50"));
876
877 let err = Error::NotInsertable {
878 table: "users".to_string(),
879 };
880 let (details, hint) = err.details_and_hint();
881 assert!(details.is_some());
882 assert!(hint.is_some());
883 assert!(details.unwrap().contains("users"));
884 }
885}