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, Clone, thiserror::Error)]
10pub enum SQLError {
11    #[error("{0}")]
12    Parse(String),
13    #[error("{0}")]
14    Unsupported(String),
15    /// The host configured this SQL session to require an independently owned notification subscription.
16    #[error("LISTEN and UNLISTEN require a notification subscription")]
17    NotificationRequiresSubscription,
18    #[error("relation \"{0}\" does not exist")]
19    UnknownTable(String),
20    #[error("column \"{0}\" does not exist")]
21    UnknownColumn(String),
22    #[error("column reference \"{0}\" is ambiguous")]
23    AmbiguousColumn(String),
24    #[error("unknown function: {0}")]
25    UnknownFunction(String),
26    #[error("type mismatch: {0}")]
27    TypeMismatch(String),
28    #[error("invalid argument count for `{name}`: expected {expected}, got {actual}")]
29    BadArity {
30        name: String,
31        expected: String,
32        actual: usize,
33    },
34    #[error("No value supplied for parameter ${0}")]
35    MissingParam(usize),
36    #[error("vector dimension mismatch: expected {expected}, got {actual}")]
37    VectorDimMismatch { expected: usize, actual: usize },
38    #[error("{0}")]
39    Cancelled(#[from] uqa_core::QueryCancelled),
40    /// Error raised by (or on behalf of) a user-defined SQL /
41    /// `PL/pgSQL` routine. Carries an explicit `SQLSTATE` so
42    /// `EXCEPTION WHEN <condition>` handlers and `SQLSTATE` /
43    /// `SQLERRM` report the same code `PostgreSQL` would.
44    #[error("{message}")]
45    Routine { sqlstate: String, message: String },
46    /// A primary SQL error with separate `PostgreSQL` diagnostic fields. `SQLERRM` and `Display` expose only the primary message; protocol clients receive detail and hint independently.
47    #[error("{message}")]
48    Diagnostic {
49        sqlstate: String,
50        message: String,
51        detail: Option<String>,
52        hint: Option<String>,
53    },
54    #[error("internal error: {0}")]
55    Internal(String),
56}
57
58impl SQLError {
59    /// Stable application error code, separate from the five-character SQLSTATE. Ordinary SQL errors do not acquire an application code.
60    pub const fn code(&self) -> Option<&'static str> {
61        match self {
62            Self::NotificationRequiresSubscription => Some("NOTIFICATION_REQUIRES_SUBSCRIPTION"),
63            _ => None,
64        }
65    }
66
67    pub fn unknown_qualified_column(qualifier: &str, column: &str) -> Self {
68        Self::Routine {
69            sqlstate: "42703".into(),
70            message: format!("column {qualifier}.{column} does not exist"),
71        }
72    }
73
74    /// `PostgreSQL` `SQLSTATE` code for the error, mirroring the
75    /// the current exception-to-state mapping. `None` for
76    /// errors that do not carry a defined `SQLSTATE`.
77    pub fn sqlstate(&self) -> Option<&str> {
78        match self {
79            SQLError::Cancelled(_) => Some(uqa_core::SQLSTATE_QUERY_CANCELED),
80            SQLError::Parse(_) => Some("42601"), // syntax_error
81            SQLError::Unsupported(_) | SQLError::NotificationRequiresSubscription => Some("0A000"), // feature_not_supported
82            SQLError::UnknownTable(_) => Some("42P01"), // undefined_table
83            SQLError::UnknownColumn(_) => Some("42703"), // undefined_column
84            SQLError::AmbiguousColumn(_) => Some("42702"), // ambiguous_column
85            SQLError::UnknownFunction(_) => Some("42883"), // undefined_function
86            SQLError::TypeMismatch(_) => Some("42804"), // datatype_mismatch
87            SQLError::BadArity { .. } => Some("42883"), // undefined_function (PG)
88            SQLError::MissingParam(_) => Some("S1002"), // ERRCODE_INVALID_PARAMETER_VALUE
89            SQLError::VectorDimMismatch { .. } => Some("22023"), // invalid_parameter_value
90            SQLError::Routine { sqlstate, .. } | SQLError::Diagnostic { sqlstate, .. } => {
91                Some(sqlstate)
92            }
93            SQLError::Internal(_) => Some("XX000"), // internal_error
94        }
95    }
96}
97
98pub type Result<T> = std::result::Result<T, SQLError>;
99
100impl From<pg_query::Error> for SQLError {
101    fn from(value: pg_query::Error) -> Self {
102        match value {
103            pg_query::Error::Parse(message)
104                if message == "WITH TIES cannot be specified without ORDER BY clause" =>
105            {
106                SQLError::Routine {
107                    sqlstate: "42601".into(),
108                    message,
109                }
110            }
111            pg_query::Error::Parse(message)
112                if message.contains("constraints cannot be altered to be NOT VALID") =>
113            {
114                SQLError::Routine {
115                    sqlstate: "0A000".into(),
116                    message,
117                }
118            }
119            pg_query::Error::Parse(message) => SQLError::Parse(message),
120            other => SQLError::Parse(other.to_string()),
121        }
122    }
123}
124
125impl From<uqa_core::memory::MemoryError> for SQLError {
126    fn from(error: uqa_core::memory::MemoryError) -> Self {
127        Self::Routine {
128            sqlstate: "53200".into(),
129            message: error.to_string(),
130        }
131    }
132}
133
134impl From<uqa_core::ValueRetentionError> for SQLError {
135    fn from(error: uqa_core::ValueRetentionError) -> Self {
136        match error {
137            uqa_core::ValueRetentionError::Memory(error) => error.into(),
138            uqa_core::ValueRetentionError::Cancelled(error) => error.into(),
139        }
140    }
141}