Skip to main content

uqa_sql/
error.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Error types surfaced by the SQL compiler and executor.
8
9#[derive(Debug, thiserror::Error)]
10pub enum SQLError {
11    #[error("{0}")]
12    Parse(String),
13    #[error("{0}")]
14    Unsupported(String),
15    #[error("relation \"{0}\" does not exist")]
16    UnknownTable(String),
17    #[error("column \"{0}\" does not exist")]
18    UnknownColumn(String),
19    #[error("column reference \"{0}\" is ambiguous")]
20    AmbiguousColumn(String),
21    #[error("unknown function: {0}")]
22    UnknownFunction(String),
23    #[error("type mismatch: {0}")]
24    TypeMismatch(String),
25    #[error("invalid argument count for `{name}`: expected {expected}, got {actual}")]
26    BadArity {
27        name: String,
28        expected: String,
29        actual: usize,
30    },
31    #[error("No value supplied for parameter ${0}")]
32    MissingParam(usize),
33    #[error("vector dimension mismatch: expected {expected}, got {actual}")]
34    VectorDimMismatch { expected: usize, actual: usize },
35    #[error("{0}")]
36    Cancelled(#[from] uqa_core::QueryCancelled),
37    /// Error raised by (or on behalf of) a user-defined SQL /
38    /// `PL/pgSQL` routine. Carries an explicit `SQLSTATE` so
39    /// `EXCEPTION WHEN <condition>` handlers and `SQLSTATE` /
40    /// `SQLERRM` report the same code `PostgreSQL` would.
41    #[error("{message}")]
42    Routine { sqlstate: String, message: String },
43    /// A primary SQL error with separate `PostgreSQL` diagnostic fields. `SQLERRM` and `Display` expose only the primary message; protocol clients receive detail and hint independently.
44    #[error("{message}")]
45    Diagnostic {
46        sqlstate: String,
47        message: String,
48        detail: Option<String>,
49        hint: Option<String>,
50    },
51    #[error("internal error: {0}")]
52    Internal(String),
53}
54
55impl SQLError {
56    pub fn unknown_qualified_column(qualifier: &str, column: &str) -> Self {
57        Self::Routine {
58            sqlstate: "42703".into(),
59            message: format!("column {qualifier}.{column} does not exist"),
60        }
61    }
62
63    /// `PostgreSQL` `SQLSTATE` code for the error, mirroring the
64    /// the current exception-to-state mapping. `None` for
65    /// errors that do not carry a defined `SQLSTATE`.
66    pub fn sqlstate(&self) -> Option<&str> {
67        match self {
68            SQLError::Cancelled(_) => Some(uqa_core::SQLSTATE_QUERY_CANCELED),
69            SQLError::Parse(_) => Some("42601"), // syntax_error
70            SQLError::Unsupported(_) => Some("0A000"), // feature_not_supported
71            SQLError::UnknownTable(_) => Some("42P01"), // undefined_table
72            SQLError::UnknownColumn(_) => Some("42703"), // undefined_column
73            SQLError::AmbiguousColumn(_) => Some("42702"), // ambiguous_column
74            SQLError::UnknownFunction(_) => Some("42883"), // undefined_function
75            SQLError::TypeMismatch(_) => Some("42804"), // datatype_mismatch
76            SQLError::BadArity { .. } => Some("42883"), // undefined_function (PG)
77            SQLError::MissingParam(_) => Some("S1002"), // ERRCODE_INVALID_PARAMETER_VALUE
78            SQLError::VectorDimMismatch { .. } => Some("22023"), // invalid_parameter_value
79            SQLError::Routine { sqlstate, .. } | SQLError::Diagnostic { sqlstate, .. } => {
80                Some(sqlstate)
81            }
82            SQLError::Internal(_) => Some("XX000"), // internal_error
83        }
84    }
85}
86
87pub type Result<T> = std::result::Result<T, SQLError>;
88
89impl From<pg_query::Error> for SQLError {
90    fn from(value: pg_query::Error) -> Self {
91        match value {
92            pg_query::Error::Parse(message)
93                if message == "WITH TIES cannot be specified without ORDER BY clause" =>
94            {
95                SQLError::Routine {
96                    sqlstate: "42601".into(),
97                    message,
98                }
99            }
100            pg_query::Error::Parse(message)
101                if message.contains("constraints cannot be altered to be NOT VALID") =>
102            {
103                SQLError::Routine {
104                    sqlstate: "0A000".into(),
105                    message,
106                }
107            }
108            pg_query::Error::Parse(message) => SQLError::Parse(message),
109            other => SQLError::Parse(other.to_string()),
110        }
111    }
112}