Skip to main content

rupg_common/
error.rs

1//! `Error`, the error of every layer, with its `SqlState`.
2//!
3//! Every error carries a SQLSTATE from the layer where it starts (spec/04 section 4.8). A storage error that reaches a client is then the SQLSTATE that PostgreSQL sends for the same cause, for example `53100` for a full disk and `XX001` for a page with a bad checksum.
4
5use std::fmt;
6use std::panic::Location;
7
8use crate::sqlstate::SqlState;
9
10#[derive(Clone, Debug)]
11pub struct Error {
12    state: SqlState,
13    message: String,
14    position: Option<usize>,
15    /// The place in the source of rupg where the error was made. The session sends it in the fields `F`, `L` and `R`, as PostgreSQL sends the place in its C source.
16    place: &'static Location<'static>,
17    /// The fields that few errors have, in a box, so that a `Result` with an `Error` stays small.
18    more: Option<Box<More>>,
19}
20
21/// The detail, the hint and the context of an error.
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
23struct More {
24    detail: Option<String>,
25    hint: Option<String>,
26    context: Option<String>,
27    /// True for a message of `ereport(NOTICE)` with a SQLSTATE outside of class `00`.
28    notice: bool,
29    /// The level of a message that is not an error, as `ereport` of `RAISE` gives it, or `None` for the level that the SQLSTATE gives.
30    level: Option<Level>,
31    /// The text of an internal query and the byte offset in it where the error starts, as `internalerrquery` and `internalerrposition` give them.
32    internal: Option<(String, Option<usize>)>,
33    /// The names of the object of the error, in the order of [`Object::ALL`].
34    objects: [Option<String>; 5],
35}
36
37/// A name of the object of an error, as `errtable`, `errtablecol`, `errdatatype` and `errconstraint` give it. A client reads it from the field with the code of [`Object::code`].
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum Object {
40    Schema,
41    Table,
42    Column,
43    Datatype,
44    Constraint,
45}
46
47impl Object {
48    /// The names in the order of the fields of a message.
49    pub const ALL: [Object; 5] =
50        [Object::Schema, Object::Table, Object::Column, Object::Datatype, Object::Constraint];
51
52    /// The code of the field of a message: `s`, `t`, `c`, `d` or `n`.
53    pub fn code(self) -> u8 {
54        match self {
55            Object::Schema => b's',
56            Object::Table => b't',
57            Object::Column => b'c',
58            Object::Datatype => b'd',
59            Object::Constraint => b'n',
60        }
61    }
62}
63
64/// The level of a message that is not an error, from the lowest to the highest.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
66pub enum Level {
67    /// `DEBUG`, which goes to the client only when `client_min_messages` is a level of `DEBUG`.
68    Debug,
69    /// `LOG`, which goes to the client only when `client_min_messages` is `log` or lower.
70    Log,
71    /// `INFO`, which always goes to the client.
72    Info,
73    /// `NOTICE`.
74    Notice,
75    /// `WARNING`.
76    Warning,
77}
78
79impl Level {
80    /// The name of the level in the field `S` of a message.
81    pub fn name(self) -> &'static str {
82        match self {
83            Level::Debug => "DEBUG",
84            Level::Log => "LOG",
85            Level::Info => "INFO",
86            Level::Notice => "NOTICE",
87            Level::Warning => "WARNING",
88        }
89    }
90}
91
92impl Error {
93    /// An error with a SQLSTATE and a primary message.
94    #[track_caller]
95    pub fn new(state: SqlState, message: impl Into<String>) -> Error {
96        Error {
97            state,
98            message: message.into(),
99            position: None,
100            place: Location::caller(),
101            more: None,
102        }
103    }
104
105    /// An `XX001` error. A check found damaged data.
106    #[track_caller]
107    pub fn corrupted(message: impl Into<String>) -> Error {
108        Error::new(SqlState::DATA_CORRUPTED, message)
109    }
110
111    /// An `XX000` error. rupg found a state that its own rules do not allow.
112    #[track_caller]
113    pub fn internal(message: impl Into<String>) -> Error {
114        Error::new(SqlState::INTERNAL_ERROR, message)
115    }
116
117    /// Adds the detail line.
118    #[must_use]
119    pub fn with_detail(mut self, detail: impl Into<String>) -> Error {
120        self.more().detail = Some(detail.into());
121        self
122    }
123
124    /// Adds the hint line.
125    #[must_use]
126    pub fn with_hint(mut self, hint: impl Into<String>) -> Error {
127        self.more().hint = Some(hint.into());
128        self
129    }
130
131    /// Adds the place in the query text where the error starts, as a byte offset. The session gives it to the client as a character position in the field `P`. An error that has a place keeps it.
132    #[must_use]
133    pub fn at(mut self, location: usize) -> Error {
134        self.position.get_or_insert(location);
135        self
136    }
137
138    /// Sets the place in the query text and drops the place that the error has, as `parser_errposition` in an error context callback does. `None` drops the place.
139    #[must_use]
140    pub fn with_position(mut self, location: Option<usize>) -> Error {
141        self.position = location;
142        self
143    }
144
145    /// Adds a line to the context, as `errcontext` does in an error context callback. The lines go from the inner place to the outer place.
146    #[must_use]
147    pub fn with_context(mut self, line: impl Into<String>) -> Error {
148        let line = line.into();
149        let more = self.more();
150        more.context = Some(match more.context.take() {
151            Some(context) => format!("{context}\n{line}"),
152            None => line,
153        });
154        self
155    }
156
157    /// Marks a message for the client as a `NOTICE`, as `ereport(NOTICE)` with a SQLSTATE such as `54000` makes it. A message without the mark is a `NOTICE` only for a SQLSTATE of class `00`.
158    #[must_use]
159    pub fn as_notice(mut self) -> Error {
160        self.more().notice = true;
161        self
162    }
163
164    /// True for a message that [`Error::as_notice`] marked.
165    pub fn is_notice(&self) -> bool {
166        self.more.as_ref().is_some_and(|more| more.notice)
167    }
168
169    /// Sets the level of a message that is not an error, as `ereport` with a level such as `INFO` gives it.
170    #[must_use]
171    pub fn with_level(mut self, level: Level) -> Error {
172        self.more().level = Some(level);
173        self
174    }
175
176    /// The level that [`Error::with_level`] set, if any.
177    pub fn level(&self) -> Option<Level> {
178        self.more.as_ref()?.level
179    }
180
181    /// Sets the internal query and the byte offset in it where the error starts, as `internalerrquery` and `internalerrposition` do. The session gives them to the client in the fields `q` and `p`.
182    #[must_use]
183    pub fn with_internal(mut self, query: impl Into<String>, position: Option<usize>) -> Error {
184        self.more().internal = Some((query.into(), position));
185        self
186    }
187
188    /// The internal query and the byte offset in it, if any.
189    pub fn internal_query(&self) -> Option<(&str, Option<usize>)> {
190        let (query, position) = self.more.as_ref()?.internal.as_ref()?;
191        Some((query, *position))
192    }
193
194    /// Sets a name of the object of the error, such as the table of a constraint that the error violates.
195    #[must_use]
196    pub fn with_object(mut self, object: Object, name: impl Into<String>) -> Error {
197        self.more().objects[object as usize] = Some(name.into());
198        self
199    }
200
201    /// The name that [`Error::with_object`] set, if any.
202    pub fn object(&self, object: Object) -> Option<&str> {
203        self.more.as_ref()?.objects[object as usize].as_deref()
204    }
205
206    fn more(&mut self) -> &mut More {
207        self.more.get_or_insert_with(Box::default)
208    }
209
210    /// The SQLSTATE.
211    pub fn state(&self) -> SqlState {
212        self.state
213    }
214
215    /// The primary message.
216    pub fn message(&self) -> &str {
217        &self.message
218    }
219
220    /// The detail, if any.
221    pub fn detail(&self) -> Option<&str> {
222        self.more.as_ref()?.detail.as_deref()
223    }
224
225    /// The hint, if any.
226    pub fn hint(&self) -> Option<&str> {
227        self.more.as_ref()?.hint.as_deref()
228    }
229
230    /// The byte offset in the query text where the error starts, if any.
231    pub fn position(&self) -> Option<usize> {
232        self.position
233    }
234
235    /// The context lines, if any.
236    pub fn context(&self) -> Option<&str> {
237        self.more.as_ref()?.context.as_deref()
238    }
239
240    /// The place in the source of rupg where the error was made.
241    pub fn place(&self) -> &'static Location<'static> {
242        self.place
243    }
244}
245
246/// Two errors are equal when they give the same fields to the client. The place in the source is not compared.
247impl PartialEq for Error {
248    fn eq(&self, other: &Error) -> bool {
249        (self.state, &self.message, self.position, &self.more)
250            == (other.state, &other.message, other.position, &other.more)
251    }
252}
253
254impl Eq for Error {}
255
256impl fmt::Display for Error {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{}: {}", self.state, self.message)?;
259        if let Some(detail) = self.detail() {
260            write!(f, "\nDETAIL: {detail}")?;
261        }
262        if let Some(hint) = self.hint() {
263            write!(f, "\nHINT: {hint}")?;
264        }
265        Ok(())
266    }
267}
268
269impl std::error::Error for Error {}
270
271/// The result type of rupg.
272pub type Result<T, E = Error> = std::result::Result<T, E>;
273
274#[cfg(test)]
275mod tests {
276    use super::{Error, Object};
277
278    #[test]
279    fn display() {
280        let e = Error::corrupted("bad checksum").with_detail("page 7").with_hint("run rupg verify");
281        assert_eq!(e.to_string(), "XX001: bad checksum\nDETAIL: page 7\nHINT: run rupg verify");
282        assert_eq!((e.position(), e.clone().at(7).at(9).position()), (None, Some(7)));
283    }
284
285    #[test]
286    fn the_place_is_where_the_error_is_made() {
287        let (e, line) = (Error::internal("bad state"), line!());
288        assert_eq!((e.place().file(), e.place().line()), (file!(), line));
289        assert_eq!(e, Error::internal("bad state"));
290    }
291
292    #[test]
293    fn the_names_of_the_object() {
294        let e =
295            Error::internal("x").with_object(Object::Table, "t").with_object(Object::Schema, "s");
296        let names: Vec<_> = Object::ALL.iter().map(|&o| e.object(o)).collect();
297        assert_eq!(names, [Some("s"), Some("t"), None, None, None]);
298        assert_ne!(e, Error::internal("x"));
299    }
300}