Skip to main content

uqa_sql/plpgsql/
raise.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Ordered RAISE diagnostic options, independent of expression execution.
8
9use super::{condition_sqlstate, runtime_diagnostics::looks_like_sqlstate, PLpgSQLExpression};
10use crate::SQLError;
11
12#[derive(Debug, Clone, Copy)]
13pub enum RaiseOptionKind {
14    ErrorCode,
15    Message,
16    Detail,
17    Hint,
18}
19
20#[derive(Debug, Clone)]
21pub struct RaiseOption {
22    pub kind: RaiseOptionKind,
23    pub value: PLpgSQLExpression,
24}
25
26#[derive(Default)]
27pub struct RaiseDiagnostic {
28    pub sqlstate: Option<String>,
29    pub condition: Option<String>,
30    pub message: Option<String>,
31    pub detail: Option<String>,
32    pub hint: Option<String>,
33}
34
35impl RaiseDiagnostic {
36    /// The executor evaluates and converts each value before checking whether that field was already supplied.
37    pub fn option(&mut self, kind: RaiseOptionKind, value: String) -> Result<(), SQLError> {
38        let (slot, name) = match kind {
39            RaiseOptionKind::ErrorCode => {
40                if self
41                    .sqlstate
42                    .as_deref()
43                    .is_some_and(|state| state != "00000")
44                {
45                    return Err(duplicate("ERRCODE"));
46                }
47                self.sqlstate = Some(
48                    if looks_like_sqlstate(&value) && !value.bytes().any(|b| b.is_ascii_lowercase())
49                    {
50                        value.clone()
51                    } else {
52                        condition_sqlstate(&value)
53                            .ok_or_else(|| SQLError::Routine {
54                                sqlstate: "42704".into(),
55                                message: format!("unrecognized exception condition \"{value}\""),
56                            })?
57                            .to_owned()
58                    },
59                );
60                self.condition = Some(value);
61                return Ok(());
62            }
63            RaiseOptionKind::Message => (&mut self.message, "MESSAGE"),
64            RaiseOptionKind::Detail => (&mut self.detail, "DETAIL"),
65            RaiseOptionKind::Hint => (&mut self.hint, "HINT"),
66        };
67        if slot.is_some() {
68            return Err(duplicate(name));
69        }
70        *slot = Some(value);
71        Ok(())
72    }
73}
74
75fn duplicate(name: &str) -> SQLError {
76    SQLError::Routine {
77        sqlstate: "42601".into(),
78        message: format!("RAISE option already specified: {name}"),
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn zero_sqlstate_can_be_replaced_but_nonzero_state_cannot() {
88        let mut diagnostic = RaiseDiagnostic::default();
89        diagnostic
90            .option(RaiseOptionKind::ErrorCode, "00000".into())
91            .unwrap();
92        diagnostic
93            .option(RaiseOptionKind::ErrorCode, "division_by_zero".into())
94            .unwrap();
95        assert_eq!(diagnostic.sqlstate.as_deref(), Some("22012"));
96        assert_eq!(
97            diagnostic
98                .option(RaiseOptionKind::ErrorCode, "22023".into())
99                .unwrap_err()
100                .sqlstate(),
101            Some("42601")
102        );
103    }
104
105    #[test]
106    fn dynamic_sqlstate_is_not_case_folded_and_empty_text_is_still_supplied() {
107        let mut diagnostic = RaiseDiagnostic::default();
108        assert_eq!(
109            diagnostic
110                .option(RaiseOptionKind::ErrorCode, "22p02".into())
111                .unwrap_err()
112                .sqlstate(),
113            Some("42704")
114        );
115        diagnostic
116            .option(RaiseOptionKind::Message, String::new())
117            .unwrap();
118        assert_eq!(
119            diagnostic
120                .option(RaiseOptionKind::Message, "another".into())
121                .unwrap_err()
122                .sqlstate(),
123            Some("42601")
124        );
125    }
126}