Skip to main content

uqa_sql/catalog/
errors.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Preserve SQL diagnostics carried through catalog storage errors.
8use crate::SQLError;
9
10pub fn storage_error(action: &str, err: &(dyn std::error::Error + 'static)) -> SQLError {
11    let mut source: Option<&(dyn std::error::Error + 'static)> = Some(err);
12    while let Some(error) = source {
13        if let Some(error) = error.downcast_ref::<SQLError>() {
14            // A diagnostic keeps its DETAIL and HINT for the client.
15            if matches!(error, SQLError::Diagnostic { .. }) {
16                return error.clone();
17            }
18            return SQLError::Routine {
19                sqlstate: error.sqlstate().unwrap_or("XX000").into(),
20                message: error.to_string(),
21            };
22        }
23        source = error.source();
24    }
25    SQLError::Internal(format!("{action} failed in storage backend: {err}"))
26}
27
28pub fn dml_storage_error(action: &str, err: impl std::fmt::Display) -> SQLError {
29    SQLError::Internal(format!("{action} failed in storage backend: {err}"))
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    #[derive(Debug)]
36    struct CatalogFailure(SQLError);
37    impl std::fmt::Display for CatalogFailure {
38        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39            formatter.write_str("catalog lookup failed")
40        }
41    }
42    impl std::error::Error for CatalogFailure {
43        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44            Some(&self.0)
45        }
46    }
47    #[test]
48    fn catalog_error_chain_preserves_the_embedded_sql_diagnostic() {
49        let original = SQLError::Routine {
50            sqlstate: "42501".into(),
51            message: "permission denied for schema secret".into(),
52        };
53        let expected = original.to_string();
54        let error = storage_error("column type coercion", &CatalogFailure(original));
55        assert_eq!(error.sqlstate(), Some("42501"));
56        assert_eq!(error.to_string(), expected);
57    }
58    #[test]
59    fn catalog_error_chain_keeps_diagnostic_detail_and_hint() {
60        let original = SQLError::Diagnostic {
61            sqlstate: "42501".into(),
62            message: "permission denied to create \"pg_catalog.t\"".into(),
63            detail: Some("System catalog modifications are currently disallowed.".into()),
64            hint: None,
65        };
66        let error = storage_error("CREATE TABLE", &CatalogFailure(original));
67        assert!(matches!(
68            error,
69            SQLError::Diagnostic { sqlstate, detail: Some(detail), .. }
70                if sqlstate == "42501" && detail == "System catalog modifications are currently disallowed."
71        ));
72    }
73    #[test]
74    fn plain_catalog_errors_retain_the_operation_context() {
75        let error = std::io::Error::other("unavailable");
76        assert!(
77            matches!(storage_error("column type coercion", &error), SQLError::Internal(message) if message == "column type coercion failed in storage backend: unavailable")
78        );
79    }
80}