1use 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 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}