1use http::StatusCode;
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
13pub enum Error {
14 #[error("Invalid path: {0}")]
18 InvalidPath(String),
19
20 #[error("Use of aggregate functions is not allowed")]
26 AggregatesNotAllowed,
27
28 #[error("Invalid query parameter: {0}")]
29 InvalidQueryParam(String),
30
31 #[error("\"failed to parse {kind} ({text})\" (line 1, column {column})")]
39 UnparsableQuery {
40 kind: &'static str,
42 text: String,
44 column: usize,
46 expected: String,
48 },
49
50 #[error("Invalid header: {0}")]
51 InvalidHeader(&'static str),
52
53 #[error("{0}")]
54 InvalidBody(String),
55
56 #[error("Unsupported HTTP method: {0}")]
57 UnsupportedMethod(String),
58
59 #[error("Cannot use the {0} method on RPC")]
65 InvalidRpcMethod(String),
66
67 #[error("Invalid schema: {requested}")]
72 UnacceptableSchema {
73 requested: String,
74 exposed: Vec<String>,
75 },
76
77 #[error("Could not find the '{column}' column of '{relation}' in the schema cache")]
78 UnknownColumn {
79 column: String,
81 relation: String,
83 },
84
85 #[error("Requested range not satisfiable")]
86 InvalidRange(String),
87
88 #[error("None of these media types are available: {0}")]
89 InvalidMediaType(String),
90
91 #[error("Missing required parameter: {0}")]
92 MissingParameter(String),
93
94 #[error("Ambiguous request: {0}")]
95 AmbiguousRequest(String),
96
97 #[error("Could not choose the best candidate function between: {}", .candidates.join(", "))]
99 AmbiguousFunction { candidates: Vec<String> },
100
101 #[error("Could not embed because more than one relationship was found for '{origin}' and '{target}'")]
103 AmbiguousRelationship {
104 origin: String,
105 target: String,
106 candidates: Vec<(String, String)>,
108 disambiguated: Vec<String>,
110 },
111
112 #[error("{0}")]
120 InvalidJwt(String),
121
122 #[error("No suitable key or wrong key type")]
128 NoSuitableJwtKey,
129
130 #[error("Server lacks JWT secret")]
133 JwtSecretMissing,
134
135 #[error("{0}")]
137 JwtClaim(String),
138
139 #[error("Anonymous access is disabled")]
140 MissingAuth,
141
142 #[error("Insufficient permissions: {0}")]
143 InsufficientPermissions(String),
144
145 #[error("Resource not found: {0}")]
149 NotFound(String),
150
151 #[error("Table not found: {name}")]
152 TableNotFound {
153 name: String,
155 suggestion: Option<String>,
157 },
158
159 #[error("Could not find the function {name}")]
165 FunctionNotFound {
166 name: String,
168 params: Vec<String>,
170 candidate: Option<String>,
172 single_param: Option<String>,
180 },
181
182 #[error("Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error")]
189 RaiseNotUnderstood(RaiseFault),
190
191 #[error("geometry column is missing")]
197 MissingGeometryColumn,
198
199 #[error("Column not found: {0}")]
200 ColumnNotFound(String),
201
202 #[error("Could not find a relationship between '{origin}' and '{target}' in the schema cache")]
203 RelationshipNotFound {
204 origin: String,
206 target: String,
208 hint: Option<String>,
210 schema: String,
212 suggestion: Option<String>,
214 },
215
216 #[error("'{0}' is not an embedded resource in this request")]
222 NotAnEmbeddedResource(String),
223
224 #[error("A related order on '{relation}' is not possible")]
228 RelatedOrderNotPossible {
229 origin: String,
231 relation: String,
233 },
234
235 #[error("Cannot coerce the result to a single JSON object")]
241 NotSingular {
242 rows: usize,
244 },
245
246 #[error("Invalid path specified in request URL")]
248 InvalidResourcePath,
249
250 #[error("Filters must include all and only primary key columns with 'eq' operators")]
255 InvalidPutFilters,
256
257 #[error("limit/offset querystring parameters are not allowed for PUT")]
259 PutLimitNotAllowed,
260
261 #[error("Payload values do not match URL in primary key column(s)")]
263 PutMatchingPk,
264
265 #[error(
267 "response.headers guc must be a JSON array composed of objects with a single key and a \
268 string value"
269 )]
270 InvalidGucHeaders,
271
272 #[error("response.status guc must be a valid status code")]
274 InvalidGucStatus,
275
276 #[error("Query result exceeds max-affected preference constraint")]
281 MaxAffectedExceeded(i64),
282
283 #[error("Invalid preferences given with handling=strict")]
287 InvalidPreferences(Vec<String>),
288
289 #[error("Schema cache not loaded")]
293 SchemaCacheNotLoaded,
294
295 #[error("Schema cache load failed: {0}")]
296 SchemaCacheLoadFailed(String),
297
298 #[error("Database error: {0}")]
302 Database(#[from] DatabaseError),
303
304 #[error("Connection pool error: {0}")]
305 ConnectionPool(String),
306
307 #[error("Internal error: {0}")]
311 Internal(String),
312
313 #[error("Configuration error: {0}")]
314 Config(String),
315
316 #[error("Invalid plan: {0}")]
320 InvalidPlan(String),
321
322 #[error("Embedding error: {0}")]
323 EmbeddingError(String),
324}
325
326impl Error {
327 pub fn status_code(&self) -> StatusCode {
329 match self {
330 Self::InvalidPath(_)
332 | Self::InvalidQueryParam(_)
333 | Self::InvalidHeader(_)
334 | Self::InvalidBody(_)
335 | Self::MissingParameter(_)
336 | Self::AmbiguousRequest(_)
337 | Self::UnparsableQuery { .. }
338 | Self::UnknownColumn { .. }
339 | Self::NotAnEmbeddedResource(_)
340 | Self::RelatedOrderNotPossible { .. }
341 | Self::InvalidPreferences(_)
342 | Self::PutLimitNotAllowed
343 | Self::PutMatchingPk
344 | Self::MaxAffectedExceeded(_)
345 | Self::InvalidPlan(_)
346 | Self::EmbeddingError(_)
347 | Self::AggregatesNotAllowed
348 | Self::MissingGeometryColumn => StatusCode::BAD_REQUEST,
349
350 Self::AmbiguousFunction { .. } | Self::AmbiguousRelationship { .. } => {
352 StatusCode::MULTIPLE_CHOICES
353 }
354
355 Self::InvalidJwt(_)
357 | Self::NoSuitableJwtKey
358 | Self::JwtClaim(_)
359 | Self::MissingAuth => StatusCode::UNAUTHORIZED,
360
361 Self::InsufficientPermissions(_) => StatusCode::FORBIDDEN,
363
364 Self::NotFound(_) | Self::TableNotFound { .. } | Self::FunctionNotFound { .. } => {
366 StatusCode::NOT_FOUND
367 }
368
369 Self::ColumnNotFound(_) | Self::RelationshipNotFound { .. } => StatusCode::BAD_REQUEST,
374
375 Self::UnsupportedMethod(_) | Self::InvalidRpcMethod(_) | Self::InvalidPutFilters => {
377 StatusCode::METHOD_NOT_ALLOWED
378 }
379
380 Self::UnacceptableSchema { .. }
382 | Self::NotSingular { .. }
383 | Self::InvalidMediaType(_) => StatusCode::NOT_ACCEPTABLE,
384
385 Self::InvalidResourcePath => StatusCode::NOT_FOUND,
386
387 Self::InvalidRange(_) => StatusCode::RANGE_NOT_SATISFIABLE,
391
392 Self::JwtSecretMissing
394 | Self::RaiseNotUnderstood(_)
395 | Self::InvalidGucHeaders
396 | Self::InvalidGucStatus
397 | Self::SchemaCacheNotLoaded
398 | Self::SchemaCacheLoadFailed(_)
399 | Self::ConnectionPool(_)
400 | Self::Internal(_)
401 | Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
402
403 Self::Database(db_err) => db_err.status_code(),
405 }
406 }
407
408 pub fn code(&self) -> &'static str {
410 match self {
411 Self::InvalidPath(_) => "PGRST100",
412 Self::AggregatesNotAllowed => "PGRST123",
413 Self::InvalidQueryParam(_) | Self::UnparsableQuery { .. } => "PGRST100",
417 Self::InvalidHeader(_) => "PGRST100",
420 Self::InvalidBody(_) => "PGRST102",
421 Self::UnsupportedMethod(_) => "PGRST117",
422 Self::InvalidRpcMethod(_) => "PGRST101",
423 Self::UnacceptableSchema { .. } => "PGRST106",
424 Self::UnknownColumn { .. } => "PGRST204",
425 Self::InvalidRange(_) => "PGRST103",
426 Self::InvalidMediaType(_) => "PGRST107",
427 Self::MissingParameter(_) => "PGRST109",
428 Self::AmbiguousRequest(_) => "PGRST110",
429 Self::AmbiguousFunction { .. } => "PGRST203",
430 Self::AmbiguousRelationship { .. } => "PGRST201",
431
432 Self::InvalidJwt(_) | Self::NoSuitableJwtKey => "PGRST301",
433 Self::JwtSecretMissing => "PGRST300",
434 Self::JwtClaim(_) => "PGRST303",
435 Self::MissingAuth => "PGRST302",
436 Self::InsufficientPermissions(_) => "PGRST203",
437
438 Self::NotFound(_) => "PGRST205",
439 Self::TableNotFound { .. } => "PGRST205",
440 Self::FunctionNotFound { .. } => "PGRST202",
441 Self::MissingGeometryColumn => "22023",
442 Self::ColumnNotFound(_) => "PGRST204",
443 Self::RelationshipNotFound { .. } => "PGRST200",
444 Self::NotAnEmbeddedResource(_) => "PGRST108",
445 Self::NotSingular { .. } => "PGRST116",
446 Self::InvalidResourcePath => "PGRST125",
447 Self::InvalidPutFilters => "PGRST105",
448 Self::PutLimitNotAllowed => "PGRST114",
449 Self::PutMatchingPk => "PGRST115",
450 Self::MaxAffectedExceeded(_) => "PGRST124",
451 Self::RaiseNotUnderstood(_) => "PGRST121",
452 Self::InvalidGucHeaders => "PGRST111",
453 Self::InvalidGucStatus => "PGRST112",
454 Self::RelatedOrderNotPossible { .. } => "PGRST118",
455 Self::InvalidPreferences(_) => "PGRST122",
456
457 Self::SchemaCacheNotLoaded => "PGRST400",
458 Self::SchemaCacheLoadFailed(_) => "PGRST401",
459
460 Self::Database(e) => e.code(),
461 Self::ConnectionPool(_) => "PGRST500",
462
463 Self::Internal(_) => "PGRST900",
464 Self::Config(_) => "PGRST901",
465
466 Self::InvalidPlan(_) => "PGRST600",
467 Self::EmbeddingError(_) => "PGRST601",
468 }
469 }
470
471 pub fn to_json(&self) -> serde_json::Value {
473 serde_json::json!({
474 "code": self.code(),
475 "message": self.to_string(),
476 "details": self.details(),
477 "hint": self.hint(),
478 })
479 }
480
481 pub fn details(&self) -> Option<serde_json::Value> {
483 match self {
484 Self::Database(db_err) => db_err.details.clone().map(serde_json::Value::String),
485 Self::AmbiguousRelationship {
488 origin,
489 target,
490 candidates,
491 ..
492 } => Some(serde_json::Value::Array(
493 candidates
494 .iter()
495 .map(|(cardinality, description)| {
496 serde_json::json!({
497 "cardinality": cardinality,
498 "embedding": format!("{} with {}", origin, target),
499 "relationship": description,
500 })
501 })
502 .collect(),
503 )),
504 Self::RelationshipNotFound {
505 origin,
506 target,
507 hint,
508 schema,
509 ..
510 } => Some(serde_json::Value::String(format!(
511 "Searched for a foreign key relationship between '{}' and '{}'{} in the schema \
512 '{}', but no matches were found.",
513 origin,
514 target,
515 match hint {
516 Some(hint) => format!(" using the hint '{}'", hint),
517 None => String::new(),
518 },
519 schema
520 ))),
521 Self::RelatedOrderNotPossible { origin, relation } => {
522 Some(serde_json::Value::String(format!(
523 "'{}' and '{}' do not form a many-to-one or one-to-one relationship",
524 origin, relation
525 )))
526 }
527 Self::InvalidRange(reason) => Some(serde_json::Value::String(reason.clone())),
528 Self::MaxAffectedExceeded(affected) => Some(serde_json::Value::String(format!(
529 "The query affects {} rows",
530 affected
531 ))),
532 Self::NotSingular { rows } => Some(serde_json::Value::String(format!(
533 "The result contains {} rows",
534 rows
535 ))),
536 Self::InvalidPreferences(invalid) => Some(serde_json::Value::String(format!(
537 "Invalid preferences: {}",
538 invalid.join(", ")
539 ))),
540 Self::RaiseNotUnderstood(fault) => Some(serde_json::Value::String(fault.details())),
541 Self::NoSuitableJwtKey => Some(serde_json::Value::String(
542 "No suitable key was found to decode the JWT".into(),
543 )),
544 Self::UnparsableQuery { expected, .. } => {
545 Some(serde_json::Value::String(expected.clone()))
546 }
547 Self::FunctionNotFound {
548 name,
549 params,
550 single_param,
551 ..
552 } => Some(serde_json::Value::String(format!(
553 "Searched for the function {} {}, but no matches were found in the schema cache.",
554 name,
555 match single_param.as_deref() {
556 Some(other) if other != JSON_PARAM =>
560 format!("with a single unnamed {} parameter", other),
561 Some(_) => format!(
562 "{} or with a single unnamed {} parameter",
563 describe_params(params),
564 JSON_PARAM
565 ),
566 None => describe_params(params),
567 }
568 ))),
569 _ => None,
570 }
571 }
572
573 pub fn hint(&self) -> Option<String> {
575 match self {
576 Self::NotAnEmbeddedResource(name) => Some(format!(
577 "Verify that '{}' is included in the 'select' query parameter.",
578 name
579 )),
580 Self::TableNotFound { suggestion, .. } => suggestion
581 .as_ref()
582 .map(|name| format!("Perhaps you meant the table '{}'", name)),
583 Self::RelationshipNotFound {
584 target, suggestion, ..
585 } => suggestion
586 .as_ref()
587 .map(|name| format!("Perhaps you meant '{}' instead of '{}'.", name, target)),
588 Self::AmbiguousFunction { .. } => Some(
589 "Try renaming the parameters or the function itself in the database so function \
590 overloading can be resolved"
591 .into(),
592 ),
593 Self::AmbiguousRelationship {
594 target,
595 disambiguated,
596 ..
597 } => Some(format!(
598 "Try changing '{}' to one of the following: {}. Find the desired relationship in \
599 the 'details' key.",
600 target,
601 disambiguated
602 .iter()
603 .map(|name| format!("'{}'", name))
604 .collect::<Vec<_>>()
605 .join(", ")
606 )),
607 Self::RaiseNotUnderstood(fault) => Some(fault.hint().into()),
608 Self::FunctionNotFound {
612 candidate,
613 single_param,
614 ..
615 } if !names_only_one_param(single_param.as_deref()) => candidate
616 .as_ref()
617 .map(|c| format!("Perhaps you meant to call the function {}", c)),
618 Self::FunctionNotFound { .. } => None,
619 Self::UnacceptableSchema { exposed, .. } => Some(format!(
623 "Only the following schemas are exposed: {}",
624 exposed.join(", ")
625 )),
626 Self::Database(db_err) => db_err.hint.clone(),
627 _ => None,
628 }
629 }
630}
631
632pub const JSON_PARAM: &str = "json/jsonb";
641
642pub fn names_only_one_param(single_param: Option<&str>) -> bool {
647 matches!(single_param, Some(other) if other != JSON_PARAM)
648}
649
650pub fn function_signature(name: &str, params: &[String]) -> String {
651 match params.is_empty() {
652 true => format!("{} without parameters", name),
653 false => format!("{}({})", name, params.join(", ")),
654 }
655}
656
657fn describe_params(params: &[String]) -> String {
658 match params.len() {
659 0 => "without parameters".to_string(),
660 1 => format!("with parameter {}", params[0]),
661 _ => format!("with parameters {}", params.join(", ")),
662 }
663}
664
665#[derive(Error, Debug)]
667#[error("Database error [{code}]: {message}")]
668pub struct DatabaseError {
669 pub code: String,
670 pub message: String,
671 pub details: Option<String>,
672 pub hint: Option<String>,
673 pub constraint: Option<String>,
674 pub table: Option<String>,
675 pub column: Option<String>,
676}
677
678#[derive(Clone, Debug)]
680pub enum RaiseFault {
681 Message(String),
683 Detail(String),
685 NoDetail,
687}
688
689impl RaiseFault {
690 pub fn details(&self) -> String {
692 match self {
693 Self::Message(raw) => format!("Invalid JSON value for MESSAGE: '{}'", raw),
694 Self::Detail(raw) => format!("Invalid JSON value for DETAIL: '{}'", raw),
695 Self::NoDetail => "DETAIL is missing in the RAISE statement".to_string(),
696 }
697 }
698
699 pub fn hint(&self) -> &'static str {
701 match self {
702 Self::Message(_) => {
703 "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and \
704 optional keys: 'details', 'hint'."
705 }
706 _ => {
707 "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and \
708 optional key: 'status_text'."
709 }
710 }
711 }
712}
713
714#[derive(Clone, Debug)]
716pub struct RaisedResponse {
717 pub status: u16,
719 pub status_text: Option<String>,
721 pub headers: Vec<(String, String)>,
723 pub body: serde_json::Value,
725}
726
727impl DatabaseError {
728 pub fn raised_response(&self) -> Option<std::result::Result<RaisedResponse, RaiseFault>> {
739 if self.code != "PGRST" {
740 return None;
741 }
742 Some(self.parse_raise())
743 }
744
745 fn parse_raise(&self) -> std::result::Result<RaisedResponse, RaiseFault> {
746 let object = |raw: &str| -> Option<serde_json::Map<String, serde_json::Value>> {
747 match serde_json::from_str(raw) {
748 Ok(serde_json::Value::Object(map)) => Some(map),
749 _ => None,
750 }
751 };
752
753 let message =
756 object(&self.message).ok_or_else(|| RaiseFault::Message(self.message.clone()))?;
757 if !message.contains_key("code") || !message.contains_key("message") {
758 return Err(RaiseFault::Message(self.message.clone()));
759 }
760
761 let raw_detail = self.details.as_deref().ok_or(RaiseFault::NoDetail)?;
762 let detail =
763 object(raw_detail).ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
764 let status = detail
765 .get("status")
766 .and_then(serde_json::Value::as_u64)
767 .ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
768 let headers = detail
769 .get("headers")
770 .and_then(serde_json::Value::as_object)
771 .ok_or_else(|| RaiseFault::Detail(raw_detail.to_string()))?;
772
773 let key = |name: &str| {
777 message
778 .get(name)
779 .cloned()
780 .unwrap_or(serde_json::Value::Null)
781 };
782
783 Ok(RaisedResponse {
784 status: status as u16,
785 status_text: detail
786 .get("status_text")
787 .and_then(serde_json::Value::as_str)
788 .map(str::to_string),
789 headers: headers
790 .iter()
791 .filter_map(|(name, value)| Some((name.clone(), value.as_str()?.to_string())))
792 .collect(),
793 body: serde_json::json!({
794 "code": key("code"),
795 "message": key("message"),
796 "details": key("details"),
797 "hint": key("hint"),
798 }),
799 })
800 }
801
802 pub fn status_code(&self) -> StatusCode {
812 let code = self.code.as_str();
813 let message = self.message.as_str();
814
815 match self.raised_response() {
816 Some(Ok(raised)) => {
817 if let Ok(status) = StatusCode::from_u16(raised.status) {
818 return status;
819 }
820 }
821 Some(Err(_)) => return StatusCode::INTERNAL_SERVER_ERROR,
824 None => {}
825 }
826
827 if let Some(digits) = code.strip_prefix("PT") {
831 if let Ok(status) = digits.parse::<u16>() {
832 if let Ok(status) = StatusCode::from_u16(status) {
833 return status;
834 }
835 }
836 return StatusCode::INTERNAL_SERVER_ERROR;
837 }
838
839 match code {
840 "23503" | "23505" => StatusCode::CONFLICT,
841 "25006" => StatusCode::METHOD_NOT_ALLOWED,
844 "21000" => match message.ends_with("requires a WHERE clause") {
848 true => StatusCode::BAD_REQUEST,
849 false => StatusCode::INTERNAL_SERVER_ERROR,
850 },
851 "22023" => match message.starts_with("role") && message.ends_with("does not exist") {
854 true => StatusCode::UNAUTHORIZED,
855 false => StatusCode::BAD_REQUEST,
856 },
857 "53400" => StatusCode::INTERNAL_SERVER_ERROR,
858 "57P01" => StatusCode::SERVICE_UNAVAILABLE,
859 "P0001" => StatusCode::BAD_REQUEST,
860 "42883" => match message.starts_with("function xmlagg(") {
863 true => StatusCode::NOT_ACCEPTABLE,
864 false => StatusCode::NOT_FOUND,
865 },
866 "42P01" => StatusCode::NOT_FOUND,
867 "42P17" => StatusCode::INTERNAL_SERVER_ERROR,
868 "42501" => StatusCode::UNAUTHORIZED,
871 _ => match code.as_bytes() {
872 [b'0', b'8', ..] => StatusCode::SERVICE_UNAVAILABLE,
873 [b'0', b'9', ..] => StatusCode::INTERNAL_SERVER_ERROR,
874 [b'0', b'L', ..] | [b'0', b'P', ..] => StatusCode::FORBIDDEN,
875 [b'2', b'5', ..] => StatusCode::INTERNAL_SERVER_ERROR,
876 [b'2', b'8', ..] => StatusCode::FORBIDDEN,
877 [b'2', b'D', ..] => StatusCode::INTERNAL_SERVER_ERROR,
878 [b'3', b'8', ..] | [b'3', b'9', ..] | [b'3', b'B', ..] => {
879 StatusCode::INTERNAL_SERVER_ERROR
880 }
881 [b'4', b'0', ..] => StatusCode::INTERNAL_SERVER_ERROR,
882 [b'5', b'3', ..] => StatusCode::SERVICE_UNAVAILABLE,
883 [b'5', b'4', ..] | [b'5', b'5', ..] | [b'5', b'7', ..] | [b'5', b'8', ..] => {
884 StatusCode::INTERNAL_SERVER_ERROR
885 }
886 [b'F', b'0', ..] | [b'H', b'V', ..] => StatusCode::INTERNAL_SERVER_ERROR,
887 [b'P', b'0', ..] => StatusCode::INTERNAL_SERVER_ERROR,
888 [b'X', b'X', ..] => StatusCode::INTERNAL_SERVER_ERROR,
889 _ => StatusCode::BAD_REQUEST,
890 },
891 }
892 }
893
894 pub fn code(&self) -> &'static str {
896 match self.code.as_str() {
897 c if c.starts_with("23") => "PGRST503", c if c.starts_with("42") => "PGRST504", c if c.starts_with("28") => "PGRST505", _ => "PGRST500", }
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn test_error_status_codes() {
911 assert_eq!(
912 Error::InvalidQueryParam("test".into()).status_code(),
913 StatusCode::BAD_REQUEST
914 );
915 assert_eq!(Error::MissingAuth.status_code(), StatusCode::UNAUTHORIZED);
916 assert_eq!(
917 Error::TableNotFound {
918 name: "users".into(),
919 suggestion: None
920 }
921 .status_code(),
922 StatusCode::NOT_FOUND
923 );
924 assert_eq!(
925 Error::UnsupportedMethod("TRACE".into()).status_code(),
926 StatusCode::METHOD_NOT_ALLOWED
927 );
928 }
929
930 #[test]
931 fn test_error_codes() {
932 assert_eq!(Error::InvalidQueryParam("test".into()).code(), "PGRST100");
935 assert_eq!(Error::MissingAuth.code(), "PGRST302");
936 assert_eq!(
937 Error::TableNotFound {
938 name: "users".into(),
939 suggestion: None
940 }
941 .code(),
942 "PGRST205"
943 );
944 }
945
946 #[test]
947 fn test_database_error_status() {
948 let constraint_error = DatabaseError {
949 code: "23505".into(), message: "Duplicate key".into(),
951 details: None,
952 hint: None,
953 constraint: Some("users_pkey".into()),
954 table: Some("users".into()),
955 column: None,
956 };
957 assert_eq!(constraint_error.status_code(), StatusCode::CONFLICT);
958 }
959
960 #[test]
961 fn test_error_to_json() {
962 let error = Error::InvalidQueryParam("bad filter".into());
963 let json = error.to_json();
964 assert_eq!(json["code"], "PGRST100");
965 assert!(json["message"].as_str().unwrap().contains("bad filter"));
966 }
967}