pgx_pg_sys/submodules/
pg_try.rs

1use crate::errcodes::PgSqlErrorCode;
2use crate::panic::{downcast_panic_payload, CaughtError};
3use std::collections::BTreeMap;
4use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe, RefUnwindSafe, UnwindSafe};
5
6/// [`PgTryBuilder`] is a mechanism to mimic Postgres C macros `PG_TRY` and `PG_CATCH`.
7///
8/// A primary difference is that the [`PgTryBuilder::finally()`] block runs even if a catch handler
9/// rethrows (or throws a new) error.
10pub struct PgTryBuilder<'a, R, F: FnOnce() -> R + UnwindSafe> {
11    func: F,
12    when: BTreeMap<
13        PgSqlErrorCode,
14        Box<dyn FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe>,
15    >,
16    others: Option<Box<dyn FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe>>,
17    rust: Option<Box<dyn FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe>>,
18    finally: Option<Box<dyn FnMut() + 'a>>,
19}
20
21impl<'a, R, F: FnOnce() -> R + UnwindSafe> PgTryBuilder<'a, R, F> {
22    /// Create a new `[PgTryBuilder]`.  The `func` argument specifies the closure that is to run.
23    ///
24    /// If it fails with either a Rust panic or a Postgres error, a registered catch handler
25    /// for that specific error is run.  Whether one exists or not, the finally block also runs
26    /// at the end, for any necessary cleanup.
27    ///
28    /// ## Example
29    ///
30    /// ```rust,no_run
31    /// # use pgx_pg_sys::{ereport, PgTryBuilder};
32    /// # use pgx_pg_sys::errcodes::PgSqlErrorCode;
33    ///
34    /// let i = 41;
35    /// let mut finished = false;
36    /// let result = PgTryBuilder::new(|| {
37    ///     if i < 42 {
38    ///         ereport!(ERROR, PgSqlErrorCode::ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE, "number too small");
39    ///     }
40    ///     i
41    /// })
42    ///     .catch_when(PgSqlErrorCode::ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE, |cause| cause.rethrow())
43    ///     .finally(|| finished = true)
44    ///     .execute();
45    ///
46    /// assert_eq!(finished, true);
47    /// assert_eq!(result, 42);
48    /// ```
49    #[must_use = "must call `PgTryBuilder::execute(self)` in order for it to run"]
50    pub fn new(func: F) -> Self {
51        Self { func, when: Default::default(), others: None, rust: None, finally: None }
52    }
53
54    /// Add a catch handler to run should a specific error occur during execution.
55    ///
56    /// The argument to the catch handler closure is a [`CaughtError`] which can be
57    /// rethrown via [`CaughtError::rethrow()`]
58    ///
59    /// The return value must be of the same type as the main execution block, or the supplied
60    /// error cause must be rethrown.
61    ///
62    /// ## Safety
63    ///
64    /// While this function isn't itself unsafe, catching and then ignoring an important internal
65    /// Postgres error may very well leave your database in an undesirable state.  This is your
66    /// responsibility.
67    #[must_use = "must call `PgTryBuilder::execute(self)` in order for it to run"]
68    pub fn catch_when(
69        mut self,
70        error: PgSqlErrorCode,
71        f: impl FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe,
72    ) -> Self {
73        self.when.insert(error, Box::new(f));
74        self
75    }
76
77    /// Add a catch-all handler to catch a raised error that wasn't explicitly caught via
78    /// [PgTryBuilder::catch_when].
79    ///
80    /// The argument to the catch handler closure is a [`CaughtError`] which can be
81    /// rethrown via [`CaughtError::rethrow()`].
82    ///
83    /// The return value must be of the same type as the main execution block, or the supplied
84    /// error cause must be rethrown.
85    ///
86    /// ## Safety
87    ///
88    /// While this function isn't itself unsafe, catching and then ignoring an important internal
89    /// Postgres error may very well leave your database in an undesirable state.  This is your
90    /// responsibility.
91    #[must_use = "must call `PgTryBuilder::execute(self)` in order for it to run"]
92    pub fn catch_others(
93        mut self,
94        f: impl FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe,
95    ) -> Self {
96        self.others = Some(Box::new(f));
97        self
98    }
99
100    /// Add a handler to specifically respond to a Rust panic.
101    ///
102    /// The catch handler's closure argument is a [`CaughtError::PostgresError {ereport, payload}`]
103    /// that you can inspect in whatever way makes sense.
104    ///
105    /// The return value must be of the same type as the main execution block, or the supplied
106    /// error cause must be rethrown.
107    ///
108    #[must_use = "must call `PgTryBuilder::execute(self)` in order for it to run"]
109    pub fn catch_rust_panic(
110        mut self,
111        f: impl FnMut(CaughtError) -> R + 'a + UnwindSafe + RefUnwindSafe,
112    ) -> Self {
113        self.rust = Some(Box::new(f));
114        self
115    }
116
117    /// The finally block, of which there can be only one.  Successive calls to this function
118    /// will replace the prior finally block.
119    ///
120    /// The finally block closure is called after successful return from the main execution handler
121    /// or a catch handler.  The finally block does not return a value.
122    #[must_use = "must call `PgTryBuilder::execute(self)` in order for it to run"]
123    pub fn finally(mut self, f: impl FnMut() + 'a) -> Self {
124        self.finally = Some(Box::new(f));
125        self
126    }
127
128    /// Run the main execution block closure.  Any error raised will be passed to a registered
129    /// catch handler, and when finished, the finally block will be run.
130    pub fn execute(mut self) -> R {
131        let result = catch_unwind(self.func);
132
133        fn finally<F: FnMut()>(f: &mut Option<F>) {
134            if let Some(f) = f {
135                f()
136            }
137        }
138
139        let result = match result {
140            Ok(result) => result,
141            Err(error) => {
142                let (sqlerrcode, root_cause) = match downcast_panic_payload(error) {
143                    CaughtError::RustPanic { ereport, payload } => {
144                        let sqlerrcode = ereport.inner.sqlerrcode;
145                        let panic = CaughtError::RustPanic { ereport, payload };
146                        (sqlerrcode, panic)
147                    }
148                    CaughtError::ErrorReport(ereport) => {
149                        let sqlerrcode = ereport.inner.sqlerrcode;
150                        let panic = CaughtError::ErrorReport(ereport);
151                        (sqlerrcode, panic)
152                    }
153                    CaughtError::PostgresError(ereport) => {
154                        let sqlerrcode = ereport.inner.sqlerrcode;
155                        let panic = CaughtError::PostgresError(ereport);
156                        (sqlerrcode, panic)
157                    }
158                };
159
160                // Postgres source docs says that a PG_TRY/PG_CATCH/PG_FINALLY block can't have
161                // both a CATCH and a FINALLY.
162                //
163                // We allow it by wrapping handler execution in its own `catch_unwind()` block
164                // and deferring the finally block execution until after it is complete
165                let handler_result = catch_unwind(AssertUnwindSafe(|| {
166                    if let Some(mut handler) = self.when.remove(&sqlerrcode) {
167                        // we have a registered catch handler for the error code we caught
168                        return handler(root_cause);
169                    } else if let Some(mut handler) = self.others {
170                        // we have a registered "catch others" handler
171                        return handler(root_cause);
172                    } else if let Some(mut handler) = self.rust {
173                        // we have a registered catch handler for a rust panic
174                        if let cause @ CaughtError::RustPanic { .. } = root_cause {
175                            // and we have a rust panic
176                            return handler(cause);
177                        }
178                    }
179
180                    // we have no handler capable of handling whatever error we have, so rethrow the root cause
181                    root_cause.rethrow();
182                }));
183
184                let handler_result = match handler_result {
185                    Ok(result) => result,
186                    Err(caught) => {
187                        let catch_handler_error = downcast_panic_payload(caught);
188
189                        // make sure to run the finally block and then resume unwinding
190                        // with this new panic
191                        finally(&mut self.finally);
192                        resume_unwind(Box::new(catch_handler_error))
193                    }
194                };
195
196                // Being here means the catch handler didn't raise an error.
197                //
198                // Postgres says:
199                unsafe {
200                    /*
201                     * FlushErrorState --- flush the error state after error recovery
202                     *
203                     * This should be called by an error handler after it's done processing
204                     * the error; or as soon as it's done CopyErrorData, if it intends to
205                     * do stuff that is likely to provoke another error.  You are not "out" of
206                     * the error subsystem until you have done this.
207                     */
208                    crate::FlushErrorState();
209                }
210                handler_result
211            }
212        };
213
214        // `result` could be from a successful execution or returned from a catch handler
215        //
216        // Either way, the finally block needs to be run before we return it
217        finally(&mut self.finally);
218        result
219    }
220}