Skip to main content

wasi_pg_client/error/
server.rs

1//! PostgreSQL server error with full field mapping.
2//!
3//! This module defines [`PgServerError`] which captures every field from a
4//! PostgreSQL `ErrorResponse` message, providing structured access to severity,
5//! SQLSTATE code, position, constraint names, and more.
6
7use crate::protocol::backend::{ErrorResponseBody, NoticeResponseBody};
8use fallible_iterator::FallibleIterator;
9
10// ---------------------------------------------------------------------------
11// PgServerError
12// ---------------------------------------------------------------------------
13
14/// A structured error returned by the PostgreSQL server.
15///
16/// Every field corresponds to a field in the PostgreSQL `ErrorResponse` or
17/// `NoticeResponse` message format.  Only `severity`, `code`, and `message`
18/// are always present; all other fields are `Option`.
19///
20/// # SQLSTATE codes
21///
22/// The `code` field contains the 5-character SQLSTATE error code defined by
23/// the SQL standard and extended by PostgreSQL.  See [`sqlstate`](super::sqlstate)
24/// for constants and helper methods.
25///
26/// # Example
27///
28/// ```ignore
29/// match err {
30///     PgError::Server(e) => {
31///         if e.is_unique_violation() {
32///             println!("duplicate key: {}", e.constraint().unwrap_or_default());
33///         }
34///     }
35///     _ => {}
36/// }
37/// ```
38#[derive(Debug, Clone, Default)]
39#[non_exhaustive]
40pub struct PgServerError {
41    /// Severity: `ERROR`, `FATAL`, `PANIC`, `WARNING`, `NOTICE`, `DEBUG`, `INFO`, `LOG`.
42    pub severity: String,
43    /// Localized severity (PostgreSQL ≥ 9.6).
44    pub severity_v: Option<String>,
45    /// SQLSTATE error code (e.g., `"23505"` for unique violation).
46    pub code: String,
47    /// Primary error message.
48    pub message: String,
49    /// Optional detail providing additional context.
50    pub detail: Option<String>,
51    /// Optional suggestion for resolving the error.
52    pub hint: Option<String>,
53    /// Error position in the query string (1-based character offset).
54    pub position: Option<u32>,
55    /// Internal position (position within internally-generated query).
56    pub internal_position: Option<u32>,
57    /// Internal query (the internally-generated command that led to the error).
58    pub internal_query: Option<String>,
59    /// Call-stack context (e.g., in PL/pgSQL functions).
60    pub where_: Option<String>,
61    /// Schema name (if applicable).
62    pub schema: Option<String>,
63    /// Table name (if applicable).
64    pub table: Option<String>,
65    /// Column name (if applicable).
66    pub column: Option<String>,
67    /// Data type name (if applicable).
68    pub data_type: Option<String>,
69    /// Constraint name (if applicable).
70    pub constraint: Option<String>,
71    /// Source file in the PostgreSQL server code.
72    pub file: Option<String>,
73    /// Source line number in the PostgreSQL server code.
74    pub line: Option<u32>,
75    /// Source routine in the PostgreSQL server code.
76    pub routine: Option<String>,
77}
78
79impl PgServerError {
80    /// Parse a [`PgServerError`] from the raw `(type_byte, value)` field pairs
81    /// of a PostgreSQL `ErrorResponse` or `NoticeResponse` message.
82    pub fn from_fields(fields: Vec<(u8, String)>) -> Self {
83        let mut err = PgServerError::default();
84        for (code, value) in fields {
85            match code {
86                b'S' => err.severity = value,
87                b'V' => err.severity_v = Some(value),
88                b'C' => err.code = value,
89                b'M' => err.message = value,
90                b'D' => err.detail = Some(value),
91                b'H' => err.hint = Some(value),
92                b'P' => err.position = value.parse().ok(),
93                b'p' => err.internal_position = value.parse().ok(),
94                b'q' => err.internal_query = Some(value),
95                b'W' => err.where_ = Some(value),
96                b's' => err.schema = Some(value),
97                b't' => err.table = Some(value),
98                b'c' => err.column = Some(value),
99                b'd' => err.data_type = Some(value),
100                b'n' => err.constraint = Some(value),
101                b'F' => err.file = Some(value),
102                b'L' => err.line = value.parse().ok(),
103                b'R' => err.routine = Some(value),
104                _ => {} // ignore unknown fields (forward-compatible)
105            }
106        }
107        err
108    }
109
110    /// Parse a [`PgServerError`] from an [`ErrorResponseBody`] (wire message).
111    pub fn from_error_body(body: &ErrorResponseBody) -> Result<Self, std::io::Error> {
112        let mut fields = Vec::new();
113        let mut iter = body.fields();
114        while let Some(field) = iter.next()? {
115            let value = std::str::from_utf8(field.value_bytes())
116                .unwrap_or("")
117                .to_string();
118            fields.push((field.type_(), value));
119        }
120        Ok(Self::from_fields(fields))
121    }
122
123    /// Parse a [`PgServerError`] from a [`NoticeResponseBody`] (wire message).
124    pub fn from_notice_body(body: &NoticeResponseBody) -> Result<Self, std::io::Error> {
125        let mut fields = Vec::new();
126        let mut iter = body.fields();
127        while let Some(field) = iter.next()? {
128            let value = std::str::from_utf8(field.value_bytes())
129                .unwrap_or("")
130                .to_string();
131            fields.push((field.type_(), value));
132        }
133        Ok(Self::from_fields(fields))
134    }
135
136    /// Returns the SQLSTATE error code (e.g., `"23505"` for unique violation).
137    ///
138    /// This is a convenience accessor equivalent to accessing the `code` field.
139    pub fn code(&self) -> &str {
140        &self.code
141    }
142
143    // =======================================================================
144    // SQLSTATE classification helpers
145    // =======================================================================
146
147    /// Check if the SQLSTATE code belongs to the given 2-character class.
148    ///
149    /// SQLSTATE codes are 5 characters.  The first two characters identify
150    /// the error class.  For example, class `"23"` is integrity constraint
151    /// violations, class `"42"` is syntax error or access violation.
152    pub fn is_class(&self, class: &str) -> bool {
153        self.code.starts_with(class)
154    }
155
156    /// Integrity constraint violation (SQLSTATE class `23`).
157    pub fn is_integrity_constraint_violation(&self) -> bool {
158        self.is_class("23")
159    }
160
161    /// Unique constraint violation (`23505`).
162    pub fn is_unique_violation(&self) -> bool {
163        self.code == "23505"
164    }
165
166    /// Foreign key constraint violation (`23503`).
167    pub fn is_foreign_key_violation(&self) -> bool {
168        self.code == "23503"
169    }
170
171    /// NOT NULL constraint violation (`23502`).
172    pub fn is_not_null_violation(&self) -> bool {
173        self.code == "23502"
174    }
175
176    /// Check constraint violation (`23514`).
177    pub fn is_check_violation(&self) -> bool {
178        self.code == "23514"
179    }
180
181    /// Exclusion constraint violation (`23P01`).
182    pub fn is_exclusion_violation(&self) -> bool {
183        self.code == "23P01"
184    }
185
186    /// Syntax error or access violation (SQLSTATE class `42`).
187    pub fn is_syntax_error(&self) -> bool {
188        self.is_class("42")
189    }
190
191    /// Insufficient privilege (`42501`).
192    pub fn is_insufficient_privilege(&self) -> bool {
193        self.code == "42501"
194    }
195
196    /// Undefined table (`42P01`).
197    pub fn is_undefined_table(&self) -> bool {
198        self.code == "42P01"
199    }
200
201    /// Undefined column (`42703`).
202    pub fn is_undefined_column(&self) -> bool {
203        self.code == "42703"
204    }
205
206    /// Serialization failure (`40001`).
207    pub fn is_serialization_failure(&self) -> bool {
208        self.code == "40001"
209    }
210
211    /// Deadlock detected (`40P01`).
212    pub fn is_deadlock_detected(&self) -> bool {
213        self.code == "40P01"
214    }
215
216    /// Connection exception (SQLSTATE class `08`).
217    pub fn is_connection_exception(&self) -> bool {
218        self.is_class("08")
219    }
220
221    /// Connection does not exist (`08003`).
222    pub fn is_connection_does_not_exist(&self) -> bool {
223        self.code == "08003"
224    }
225
226    /// Connection failure (`08006`).
227    pub fn is_connection_failure(&self) -> bool {
228        self.code == "08006"
229    }
230
231    /// SQL client unable to establish SQL connection (`08001`).
232    pub fn is_sqlclient_unable_to_establish_sqlconnection(&self) -> bool {
233        self.code == "08001"
234    }
235
236    /// Query canceled (`57014`).
237    pub fn is_query_canceled(&self) -> bool {
238        self.code == "57014"
239    }
240
241    /// Admin shutdown (`57P01`).
242    pub fn is_admin_shutdown(&self) -> bool {
243        self.code == "57P01"
244    }
245
246    /// Crash shutdown (`57P02`).
247    pub fn is_crash_shutdown(&self) -> bool {
248        self.code == "57P02"
249    }
250
251    /// Cannot connect now (`57P03`).
252    pub fn is_cannot_connect_now(&self) -> bool {
253        self.code == "57P03"
254    }
255
256    /// Database dropped (`57P04`).
257    pub fn is_database_dropped(&self) -> bool {
258        self.code == "57P04"
259    }
260
261    /// Idle session timeout (`57P05`).
262    pub fn is_idle_session_timeout(&self) -> bool {
263        self.code == "57P05"
264    }
265
266    /// Returns `true` if the error severity is `FATAL` or `PANIC`.
267    pub fn is_fatal(&self) -> bool {
268        self.severity == "FATAL" || self.severity == "PANIC"
269    }
270
271    /// Returns `true` if the error severity is `WARNING` or lower
272    /// (`NOTICE`, `DEBUG`, `INFO`, `LOG`).
273    pub fn is_warning_or_less(&self) -> bool {
274        matches!(
275            self.severity.as_str(),
276            "WARNING" | "NOTICE" | "DEBUG" | "INFO" | "LOG"
277        )
278    }
279
280    // =======================================================================
281    // Convenience accessors
282    // =======================================================================
283
284    /// Returns the schema name, if available.
285    pub fn schema(&self) -> Option<&str> {
286        self.schema.as_deref()
287    }
288
289    /// Returns the table name, if available.
290    pub fn table(&self) -> Option<&str> {
291        self.table.as_deref()
292    }
293
294    /// Returns the column name, if available.
295    pub fn column(&self) -> Option<&str> {
296        self.column.as_deref()
297    }
298
299    /// Returns the constraint name, if available.
300    pub fn constraint(&self) -> Option<&str> {
301        self.constraint.as_deref()
302    }
303
304    /// Returns the detail, if available.
305    pub fn detail(&self) -> Option<&str> {
306        self.detail.as_deref()
307    }
308
309    /// Returns the hint, if available.
310    pub fn hint(&self) -> Option<&str> {
311        self.hint.as_deref()
312    }
313
314    /// Returns the error position in the query, if available.
315    pub fn position(&self) -> Option<u32> {
316        self.position
317    }
318}
319
320impl std::fmt::Display for PgServerError {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        write!(
323            f,
324            "{}: {} (SQLSTATE {})",
325            self.severity, self.message, self.code
326        )?;
327        if let Some(detail) = &self.detail {
328            write!(f, "\nDETAIL: {}", detail)?;
329        }
330        if let Some(hint) = &self.hint {
331            write!(f, "\nHINT: {}", hint)?;
332        }
333        if let Some(position) = self.position {
334            write!(f, "\nPOSITION: {}", position)?;
335        }
336        Ok(())
337    }
338}
339
340impl std::error::Error for PgServerError {}
341
342// ---------------------------------------------------------------------------
343// Tests
344// ---------------------------------------------------------------------------
345
346#[cfg(test)]
347mod tests {
348    #![allow(clippy::field_reassign_with_default)]
349
350    use super::*;
351
352    #[test]
353    fn test_from_fields_all_fields() {
354        let fields = vec![
355            (b'S', "ERROR".to_string()),
356            (b'V', "ERROR".to_string()),
357            (b'C', "23505".to_string()),
358            (
359                b'M',
360                "duplicate key value violates unique constraint".to_string(),
361            ),
362            (b'D', "Key (id)=(1) already exists.".to_string()),
363            (b'H', "Try a different value.".to_string()),
364            (b'P', "42".to_string()),
365            (b'p', "10".to_string()),
366            (b'q', "SELECT ...".to_string()),
367            (b'W', "PL/pgSQL function ...".to_string()),
368            (b's', "public".to_string()),
369            (b't', "users".to_string()),
370            (b'c', "id".to_string()),
371            (b'd', "integer".to_string()),
372            (b'n', "users_pkey".to_string()),
373            (b'F', "nbtinsert.c".to_string()),
374            (b'L', "532".to_string()),
375            (b'R', "_bt_check_unique".to_string()),
376        ];
377
378        let err = PgServerError::from_fields(fields);
379        assert_eq!(err.severity, "ERROR");
380        assert_eq!(err.severity_v.as_deref(), Some("ERROR"));
381        assert_eq!(err.code, "23505");
382        assert_eq!(
383            err.message,
384            "duplicate key value violates unique constraint"
385        );
386        assert_eq!(err.detail.as_deref(), Some("Key (id)=(1) already exists."));
387        assert_eq!(err.hint.as_deref(), Some("Try a different value."));
388        assert_eq!(err.position, Some(42));
389        assert_eq!(err.internal_position, Some(10));
390        assert_eq!(err.internal_query.as_deref(), Some("SELECT ..."));
391        assert_eq!(err.where_.as_deref(), Some("PL/pgSQL function ..."));
392        assert_eq!(err.schema.as_deref(), Some("public"));
393        assert_eq!(err.table.as_deref(), Some("users"));
394        assert_eq!(err.column.as_deref(), Some("id"));
395        assert_eq!(err.data_type.as_deref(), Some("integer"));
396        assert_eq!(err.constraint.as_deref(), Some("users_pkey"));
397        assert_eq!(err.file.as_deref(), Some("nbtinsert.c"));
398        assert_eq!(err.line, Some(532));
399        assert_eq!(err.routine.as_deref(), Some("_bt_check_unique"));
400    }
401
402    #[test]
403    fn test_from_fields_minimal() {
404        let fields = vec![
405            (b'S', "ERROR".to_string()),
406            (b'C', "42601".to_string()),
407            (b'M', "syntax error".to_string()),
408        ];
409
410        let err = PgServerError::from_fields(fields);
411        assert_eq!(err.severity, "ERROR");
412        assert_eq!(err.code, "42601");
413        assert_eq!(err.message, "syntax error");
414        assert!(err.detail.is_none());
415        assert!(err.hint.is_none());
416        assert!(err.position.is_none());
417    }
418
419    #[test]
420    fn test_from_fields_unknown_field_ignored() {
421        let fields = vec![
422            (b'S', "ERROR".to_string()),
423            (b'C', "42601".to_string()),
424            (b'M', "syntax error".to_string()),
425            (b'X', "unknown field".to_string()), // should be ignored
426        ];
427
428        let err = PgServerError::from_fields(fields);
429        assert_eq!(err.message, "syntax error");
430    }
431
432    #[test]
433    fn test_sqlstate_classification() {
434        let mut err = PgServerError::default();
435
436        // Unique violation
437        err.code = "23505".to_string();
438        assert!(err.is_unique_violation());
439        assert!(err.is_integrity_constraint_violation());
440        assert!(!err.is_syntax_error());
441
442        // Syntax error
443        err.code = "42601".to_string();
444        assert!(err.is_syntax_error());
445        assert!(!err.is_integrity_constraint_violation());
446
447        // Foreign key
448        err.code = "23503".to_string();
449        assert!(err.is_foreign_key_violation());
450
451        // NOT NULL
452        err.code = "23502".to_string();
453        assert!(err.is_not_null_violation());
454
455        // Check
456        err.code = "23514".to_string();
457        assert!(err.is_check_violation());
458
459        // Exclusion
460        err.code = "23P01".to_string();
461        assert!(err.is_exclusion_violation());
462
463        // Insufficient privilege
464        err.code = "42501".to_string();
465        assert!(err.is_insufficient_privilege());
466
467        // Undefined table
468        err.code = "42P01".to_string();
469        assert!(err.is_undefined_table());
470
471        // Undefined column
472        err.code = "42703".to_string();
473        assert!(err.is_undefined_column());
474
475        // Serialization failure
476        err.code = "40001".to_string();
477        assert!(err.is_serialization_failure());
478
479        // Deadlock
480        err.code = "40P01".to_string();
481        assert!(err.is_deadlock_detected());
482
483        // Connection exception
484        err.code = "08006".to_string();
485        assert!(err.is_connection_exception());
486        assert!(err.is_connection_failure());
487
488        // Query canceled
489        err.code = "57014".to_string();
490        assert!(err.is_query_canceled());
491    }
492
493    #[test]
494    fn test_severity_checks() {
495        let mut err = PgServerError::default();
496
497        err.severity = "FATAL".to_string();
498        assert!(err.is_fatal());
499        assert!(!err.is_warning_or_less());
500
501        err.severity = "PANIC".to_string();
502        assert!(err.is_fatal());
503
504        err.severity = "ERROR".to_string();
505        assert!(!err.is_fatal());
506        assert!(!err.is_warning_or_less());
507
508        err.severity = "WARNING".to_string();
509        assert!(err.is_warning_or_less());
510
511        err.severity = "DEBUG".to_string();
512        assert!(err.is_warning_or_less());
513
514        err.severity = "INFO".to_string();
515        assert!(err.is_warning_or_less());
516
517        err.severity = "LOG".to_string();
518        assert!(err.is_warning_or_less());
519    }
520
521    #[test]
522    fn test_display_format() {
523        let err = PgServerError::from_fields(vec![
524            (b'S', "ERROR".to_string()),
525            (b'C', "23505".to_string()),
526            (b'M', "duplicate key".to_string()),
527            (b'D', "Key (id)=(1) already exists.".to_string()),
528            (b'H', "Try a different value.".to_string()),
529            (b'P', "42".to_string()),
530        ]);
531
532        let display = err.to_string();
533        assert!(display.contains("ERROR: duplicate key (SQLSTATE 23505)"));
534        assert!(display.contains("DETAIL: Key (id)=(1) already exists."));
535        assert!(display.contains("HINT: Try a different value."));
536        assert!(display.contains("POSITION: 42"));
537    }
538
539    #[test]
540    fn test_display_format_minimal() {
541        let err = PgServerError::from_fields(vec![
542            (b'S', "ERROR".to_string()),
543            (b'C', "42601".to_string()),
544            (b'M', "syntax error".to_string()),
545        ]);
546
547        let display = err.to_string();
548        assert_eq!(display, "ERROR: syntax error (SQLSTATE 42601)");
549    }
550
551    #[test]
552    fn test_convenience_accessors() {
553        let err = PgServerError::from_fields(vec![
554            (b'S', "ERROR".to_string()),
555            (b'C', "23505".to_string()),
556            (b'M', "duplicate key".to_string()),
557            (b'D', "some detail".to_string()),
558            (b'H', "some hint".to_string()),
559            (b's', "public".to_string()),
560            (b't', "users".to_string()),
561            (b'c', "id".to_string()),
562            (b'n', "users_pkey".to_string()),
563            (b'P', "42".to_string()),
564        ]);
565
566        assert_eq!(err.schema(), Some("public"));
567        assert_eq!(err.table(), Some("users"));
568        assert_eq!(err.column(), Some("id"));
569        assert_eq!(err.constraint(), Some("users_pkey"));
570        assert_eq!(err.detail(), Some("some detail"));
571        assert_eq!(err.hint(), Some("some hint"));
572        assert_eq!(err.position(), Some(42));
573    }
574
575    #[test]
576    fn test_connection_related_codes() {
577        let mut err = PgServerError::default();
578
579        err.code = "08003".to_string();
580        assert!(err.is_connection_does_not_exist());
581
582        err.code = "08001".to_string();
583        assert!(err.is_sqlclient_unable_to_establish_sqlconnection());
584
585        err.code = "57P01".to_string();
586        assert!(err.is_admin_shutdown());
587
588        err.code = "57P02".to_string();
589        assert!(err.is_crash_shutdown());
590
591        err.code = "57P03".to_string();
592        assert!(err.is_cannot_connect_now());
593
594        err.code = "57P04".to_string();
595        assert!(err.is_database_dropped());
596
597        err.code = "57P05".to_string();
598        assert!(err.is_idle_session_timeout());
599    }
600}