Skip to main content

dbrest_core/error/
mod.rs

1//! Error handling for dbrest
2//!
3//! This module provides:
4//! - [`enum@Error`] - Main error enum with DBRST-compatible error codes
5//! - [`ErrorResponse`] - JSON response format for errors
6//!
7//! # Error Codes
8//!
9//! Error codes follow DBRST conventions:
10//! - DBRST000-099: Configuration errors
11//! - DBRST100-199: API request errors
12//! - DBRST200-299: Schema cache errors
13//! - DBRST300-399: Authentication errors
14//! - DBRST400-499: Request/action errors
15//! - DBRST500-599: Database errors
16
17pub mod codes;
18pub mod response;
19
20pub use response::ErrorResponse;
21
22use thiserror::Error;
23
24/// Main error type for dbrest
25///
26/// Each variant maps to a specific DBRST error code and HTTP status code.
27#[derive(Debug, Error)]
28pub enum Error {
29    // =========================================
30    // Configuration Errors (DBRST000-099)
31    // =========================================
32    #[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    // =========================================
45    // API Request Errors (DBRST100-199)
46    // =========================================
47    #[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    // =========================================
145    // Schema Cache Errors (DBRST200-299)
146    // =========================================
147    #[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    // =========================================
180    // JWT/Auth Errors (DBRST300-399)
181    // =========================================
182    #[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    // =========================================
195    // Request/Action Errors (DBRST400-499)
196    // =========================================
197    #[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    // =========================================
222    // Database Errors (DBRST500-599)
223    // =========================================
224    #[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    // =========================================
260    // Internal Errors
261    // =========================================
262    #[error("Internal error: {0}")]
263    Internal(String),
264}
265
266impl Error {
267    /// Get the DBRST error code for this error.
268    pub fn code(&self) -> &'static str {
269        match self {
270            // Config errors
271            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            // Request errors
277            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            // Schema errors
310            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            // Auth errors
319            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            // Action errors
325            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            // Database errors
335            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            // Internal
346            Error::Internal(_) => codes::internal::INTERNAL_ERROR,
347        }
348    }
349
350    /// Get the HTTP status code for this error.
351    pub fn status(&self) -> http::StatusCode {
352        use http::StatusCode;
353
354        match self {
355            // Config errors → 503 Service Unavailable
356            Error::DbConnection(_)
357            | Error::UnsupportedPgVersion { .. }
358            | Error::InvalidConfig { .. }
359            | Error::ConnectionRetryTimeout
360            | Error::SchemaCacheNotReady => StatusCode::SERVICE_UNAVAILABLE,
361
362            // Request parsing errors → 400 Bad Request
363            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            // Not found → 404
399            Error::TableNotFound { .. }
400            | Error::ColumnNotFound { .. }
401            | Error::FunctionNotFound { .. }
402            | Error::RelationshipNotFound { .. }
403            | Error::InvalidResourcePath
404            | Error::OpenApiDisabled => StatusCode::NOT_FOUND,
405
406            // Ambiguous errors → 300 Multiple Choices
407            Error::AmbiguousRelationship { .. } | Error::AmbiguousFunction { .. } => {
408                StatusCode::MULTIPLE_CHOICES
409            }
410
411            // Auth errors → 401/403/500
412            Error::JwtAuth(e) => e.status(),
413            Error::Jwt(_) | Error::NoAnonRole => StatusCode::UNAUTHORIZED,
414            Error::PermissionDenied { .. } => StatusCode::FORBIDDEN,
415
416            // Permission/capability errors → 405
417            Error::NotInsertable { .. }
418            | Error::NotUpdatable { .. }
419            | Error::NotDeletable { .. } => StatusCode::METHOD_NOT_ALLOWED,
420
421            // Conflict errors → 409
422            Error::NoPrimaryKey { .. }
423            | Error::UniqueViolation(_)
424            | Error::ForeignKeyViolation(_)
425            | Error::CheckViolation(_)
426            | Error::ExclusionViolation(_) => StatusCode::CONFLICT,
427
428            // Single object expected → 406
429            Error::SingleObjectExpected => StatusCode::NOT_ACCEPTABLE,
430
431            // Raised exceptions → use status from error or default to 400
432            Error::RaisedException { status, .. } => status
433                .and_then(|s| StatusCode::from_u16(s).ok())
434                .unwrap_or(StatusCode::BAD_REQUEST),
435
436            // DBRST raise → use status from error
437            Error::DbrstRaise { status, .. } => {
438                StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_REQUEST)
439            }
440
441            // General database errors → 500
442            Error::Database { .. } | Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
443        }
444    }
445
446    /// Extract additional details and hint for error response.
447    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"); // TableNotFound uses DBRST205 per PostgREST
572        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        // DBRST003
611        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        // DBRST106
621        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        // DBRST107
631        assert_eq!(
632            Error::InvalidSpreadColumn("col".to_string()).code(),
633            codes::request::INVALID_SPREAD_COLUMN
634        );
635
636        // DBRST112
637        assert_eq!(
638            Error::InvalidMediaHandler("handler".to_string()).code(),
639            codes::request::INVALID_MEDIA_HANDLER
640        );
641
642        // DBRST113
643        assert_eq!(
644            Error::MediaTypeMismatch("mismatch".to_string()).code(),
645            codes::request::MEDIA_TYPE_MISMATCH
646        );
647
648        // DBRST114
649        assert_eq!(
650            Error::UriTooLong("uri".to_string()).code(),
651            codes::request::URI_TOO_LONG
652        );
653
654        // DBRST115
655        assert_eq!(
656            Error::InvalidAggregate("agg".to_string()).code(),
657            codes::request::INVALID_AGGREGATE
658        );
659
660        // DBRST505
661        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        // DBRST506
671        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        // DBRST507
681        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        // DBRST508
699        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        // Test that all new variants serialize correctly
720        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        // Test that every error variant has a valid status code
752        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            // All status codes should be valid (not panic)
849            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        // Test details_and_hint for various error types
856        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}