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    /// Original parser diagnostics, including its one-based character position.
14    #[error("{0}")]
15    ParseDiagnostic(Box<pg_query::Diagnostic>),
16    #[error("{0}")]
17    Unsupported(String),
18    /// The host configured this SQL session to require an independently owned notification subscription.
19    #[error("LISTEN and UNLISTEN require a notification subscription")]
20    NotificationRequiresSubscription,
21    #[error("relation \"{0}\" does not exist")]
22    UnknownTable(String),
23    #[error("column \"{0}\" does not exist")]
24    UnknownColumn(String),
25    #[error("column reference \"{0}\" is ambiguous")]
26    AmbiguousColumn(String),
27    #[error("unknown function: {0}")]
28    UnknownFunction(String),
29    #[error("type mismatch: {0}")]
30    TypeMismatch(String),
31    #[error("invalid argument count for `{name}`: expected {expected}, got {actual}")]
32    BadArity {
33        name: String,
34        expected: String,
35        actual: usize,
36    },
37    #[error("No value supplied for parameter ${0}")]
38    MissingParam(usize),
39    #[error("vector dimension mismatch: expected {expected}, got {actual}")]
40    VectorDimMismatch { expected: usize, actual: usize },
41    #[error("{function_name}: column `{table}.{field}` has no text index; create one with CREATE INDEX ... ON {table} USING gin ({field})")]
42    TextIndexRequired {
43        function_name: String,
44        table: String,
45        field: String,
46    },
47    #[error("{0}")]
48    Cancelled(#[from] uqa_core::QueryCancelled),
49    /// Error raised by (or on behalf of) a user-defined SQL /
50    /// `PL/pgSQL` routine. Carries an explicit `SQLSTATE` so
51    /// `EXCEPTION WHEN <condition>` handlers and `SQLSTATE` /
52    /// `SQLERRM` report the same code `PostgreSQL` would.
53    #[error("{message}")]
54    Routine { sqlstate: String, message: String },
55    /// A primary SQL error with separate `PostgreSQL` diagnostic fields. `SQLERRM` and `Display` expose only the primary message; protocol clients receive detail and hint independently.
56    #[error("{message}")]
57    Diagnostic {
58        sqlstate: String,
59        message: String,
60        detail: Option<String>,
61        hint: Option<String>,
62    },
63    #[error("internal error: {0}")]
64    Internal(String),
65}
66
67impl SQLError {
68    /// Stable application error code, separate from the five-character SQLSTATE. Ordinary SQL errors do not acquire an application code.
69    pub const fn code(&self) -> Option<&'static str> {
70        match self {
71            Self::NotificationRequiresSubscription => Some("NOTIFICATION_REQUIRES_SUBSCRIPTION"),
72            _ => None,
73        }
74    }
75
76    /// `ParseFuncOrColumn`'s error when no function matches a call: `signature` is the name with its argument types, as `func_signature_string` spells it.
77    pub fn undefined_function_call(signature: &str) -> Self {
78        Self::Diagnostic {
79            sqlstate: "42883".into(),
80            message: format!("function {signature} does not exist"),
81            detail: None,
82            hint: Some(
83                "No function matches the given name and argument types. You might need to add explicit type casts."
84                    .into(),
85            ),
86        }
87    }
88
89    /// `ParseFuncOrColumn`'s error when more than one function matches a call equally well.
90    pub fn ambiguous_function_call(signature: &str) -> Self {
91        Self::Diagnostic {
92            sqlstate: "42725".into(),
93            message: format!("function {signature} is not unique"),
94            detail: None,
95            hint: Some(
96                "Could not choose a best candidate function. You might need to add explicit type casts."
97                    .into(),
98            ),
99        }
100    }
101
102    /// A failed call resolution in `ParseFuncOrColumn`'s terms: `42883` when no function matches and `42725` when no candidate is best, each with its hint.
103    pub fn function_call_resolution(sqlstate: &str, signature: &str, suffix: &str) -> Self {
104        match (sqlstate, suffix) {
105            ("42883", "does not exist") => Self::undefined_function_call(signature),
106            ("42725", "is not unique") => Self::ambiguous_function_call(signature),
107            _ => Self::Routine {
108                sqlstate: sqlstate.into(),
109                message: format!("function {signature} {suffix}"),
110            },
111        }
112    }
113
114    pub fn unknown_qualified_column(qualifier: &str, column: &str) -> Self {
115        Self::Routine {
116            sqlstate: "42703".into(),
117            message: format!("column {qualifier}.{column} does not exist"),
118        }
119    }
120
121    /// `PostgreSQL` `SQLSTATE` code for the error, mirroring the
122    /// the current exception-to-state mapping. `None` for
123    /// errors that do not carry a defined `SQLSTATE`.
124    pub fn sqlstate(&self) -> Option<&str> {
125        match self {
126            SQLError::Cancelled(cancelled) => Some(cancelled.sqlstate()),
127            SQLError::Parse(_) => Some("42601"), // syntax_error
128            SQLError::ParseDiagnostic(diagnostic) => Some(&diagnostic.sqlstate),
129            SQLError::Unsupported(_) | SQLError::NotificationRequiresSubscription => Some("0A000"), // feature_not_supported
130            SQLError::UnknownTable(_) => Some("42P01"), // undefined_table
131            SQLError::UnknownColumn(_) => Some("42703"), // undefined_column
132            SQLError::AmbiguousColumn(_) => Some("42702"), // ambiguous_column
133            SQLError::UnknownFunction(_) => Some("42883"), // undefined_function
134            SQLError::TypeMismatch(_) | SQLError::TextIndexRequired { .. } => Some("42804"), // datatype_mismatch
135            SQLError::BadArity { .. } => Some("42883"), // undefined_function (PG)
136            SQLError::MissingParam(_) => Some("S1002"), // ERRCODE_INVALID_PARAMETER_VALUE
137            SQLError::VectorDimMismatch { .. } => Some("22023"), // invalid_parameter_value
138            SQLError::Routine { sqlstate, .. } | SQLError::Diagnostic { sqlstate, .. } => {
139                Some(sqlstate)
140            }
141            SQLError::Internal(_) => Some("XX000"), // internal_error
142        }
143    }
144
145    /// `PostgreSQL` DETAIL field, reported separately from the primary message.
146    pub fn detail(&self) -> Option<&str> {
147        match self {
148            SQLError::Diagnostic { detail, .. } => detail.as_deref(),
149            SQLError::ParseDiagnostic(diagnostic) => diagnostic.detail.as_deref(),
150            _ => None,
151        }
152    }
153
154    /// `PostgreSQL` HINT field, reported separately from the primary message.
155    pub fn hint(&self) -> Option<&str> {
156        match self {
157            SQLError::Diagnostic { hint, .. } => hint.as_deref(),
158            SQLError::ParseDiagnostic(diagnostic) => diagnostic.hint.as_deref(),
159            _ => None,
160        }
161    }
162
163    /// One-based character position in the submitted SQL, only when supplied by the parser.
164    pub fn position(&self) -> Option<u32> {
165        match self {
166            Self::ParseDiagnostic(diagnostic) => u32::try_from(diagnostic.cursor_position)
167                .ok()
168                .filter(|position| *position != 0),
169            _ => None,
170        }
171    }
172}
173
174pub type Result<T> = std::result::Result<T, SQLError>;
175
176impl From<pg_query::Error> for SQLError {
177    fn from(value: pg_query::Error) -> Self {
178        match value {
179            pg_query::Error::ParseDiagnostic(diagnostic) => Self::ParseDiagnostic(diagnostic),
180            pg_query::Error::Parse(message) => Self::Parse(message),
181            other => Self::Parse(other.to_string()),
182        }
183    }
184}
185
186impl From<uqa_core::memory::MemoryError> for SQLError {
187    fn from(error: uqa_core::memory::MemoryError) -> Self {
188        Self::Routine {
189            sqlstate: "53200".into(),
190            message: error.to_string(),
191        }
192    }
193}
194
195impl From<uqa_core::ValueRetentionError> for SQLError {
196    fn from(error: uqa_core::ValueRetentionError) -> Self {
197        match error {
198            uqa_core::ValueRetentionError::Memory(error) => error.into(),
199            uqa_core::ValueRetentionError::Cancelled(error) => error.into(),
200            error @ uqa_core::ValueRetentionError::Malformed { .. } => Self::Routine {
201                sqlstate: "XX001".into(),
202                message: error.to_string(),
203            },
204        }
205    }
206}