uqa_sql/plpgsql/
runtime_diagnostics.rs1use crate::{expr::cast_value, plpgsql::condition_sqlstates, SQLError, SQLResult};
10use uqa_core::Value;
11
12pub fn return_query_context_error() -> SQLError {
13 SQLError::Routine {
14 sqlstate: "42601".into(),
15 message: "cannot use RETURN QUERY in a non-SETOF function".into(),
16 }
17}
18
19pub fn result_row_values(result: &SQLResult, row: usize) -> Option<Vec<Value>> {
21 (row < result.rows.len()).then(|| {
22 (0..result.columns.len())
23 .map(|column| result.value_at(row, column).cloned().unwrap_or(Value::Null))
24 .collect()
25 })
26}
27
28pub fn result_row_count(result: &SQLResult) -> Result<i64, SQLError> {
29 let (raw_count, source) = if result.columns.is_empty() {
30 (result.affected_rows, "affected-row")
31 } else {
32 (
33 u64::try_from(result.rows.len()).map_err(|_| {
34 SQLError::Internal(format!(
35 "result row count {} cannot be represented as u64",
36 result.rows.len()
37 ))
38 })?,
39 "result-row",
40 )
41 };
42 i64::try_from(raw_count).map_err(|_| {
43 SQLError::Internal(format!(
44 "{source} count {raw_count} exceeds PL/pgSQL's signed 64-bit ROW_COUNT range"
45 ))
46 })
47}
48
49pub fn strict_into_check(row_count: i64) -> Result<(), SQLError> {
50 if row_count == 0 {
51 return Err(SQLError::Routine {
52 sqlstate: "P0002".into(),
53 message: "query returned no rows".into(),
54 });
55 }
56 if row_count > 1 {
57 return Err(SQLError::Routine {
58 sqlstate: "P0003".into(),
59 message: "query returned more than one row".into(),
60 });
61 }
62 Ok(())
63}
64
65pub fn to_i64_value(value: &Value) -> Result<i64, SQLError> {
66 match cast_value(value, "bigint")? {
67 Value::Int(v) => Ok(v),
68 other => Err(SQLError::TypeMismatch(format!(
69 "expected an integer, got {other:?}"
70 ))),
71 }
72}
73
74pub fn catchable(error: &SQLError) -> bool {
75 !matches!(error, SQLError::Cancelled(_))
76}
77
78pub fn routine_message(error: &SQLError) -> String {
81 match error {
82 SQLError::Routine { message, .. } => message.clone(),
83 other => other.to_string(),
84 }
85}
86
87pub fn looks_like_sqlstate(text: &str) -> bool {
88 text.len() == 5 && text.bytes().all(|b| b.is_ascii_alphanumeric())
89}
90
91pub fn arm_matches(conditions: &[String], state: &str) -> Result<bool, SQLError> {
93 for condition in conditions {
94 if condition == "others" {
95 if state != "57014" && state != "P0004" {
98 return Ok(true);
99 }
100 continue;
101 }
102 let mut known_condition = false;
103 for mapped in condition_sqlstates(condition) {
104 known_condition = true;
105 if sqlstate_matches(mapped, state) {
106 return Ok(true);
107 }
108 }
109 if !known_condition {
110 if looks_like_sqlstate(condition) {
111 if sqlstate_matches(&condition.to_ascii_uppercase(), state) {
112 return Ok(true);
113 }
114 } else {
115 return Err(SQLError::Internal(format!(
116 "unrecognized PL/pgSQL exception condition `{condition}`"
117 )));
118 }
119 }
120 }
121 Ok(false)
122}
123
124pub fn sqlstate_matches(condition: &str, state: &str) -> bool {
125 condition == state || (condition.ends_with("000") && state.get(..2) == condition.get(..2))
126}
127
128pub fn format_raise_message(format: &str, args: &[Value]) -> Result<String, SQLError> {
130 let mut out = String::with_capacity(format.len() + 16);
131 let mut chars = format.chars().peekable();
132 let mut next_arg = 0usize;
133 while let Some(c) = chars.next() {
134 if c != '%' {
135 out.push(c);
136 continue;
137 }
138 if chars.peek() == Some(&'%') {
139 chars.next();
140 out.push('%');
141 continue;
142 }
143 let Some(value) = args.get(next_arg) else {
144 return Err(SQLError::Routine {
145 sqlstate: "42601".into(),
146 message: "too few parameters specified for RAISE".into(),
147 });
148 };
149 next_arg += 1;
150 out.push_str(&raise_text(value));
151 }
152 if next_arg < args.len() {
153 return Err(SQLError::Routine {
154 sqlstate: "42601".into(),
155 message: "too many parameters specified for RAISE".into(),
156 });
157 }
158 Ok(out)
159}
160
161pub fn raise_text(value: &Value) -> String {
164 match value {
165 Value::Null => "<NULL>".into(),
166 Value::Void => String::new(),
167 Value::Bool(b) => (if *b { "t" } else { "f" }).into(),
168 Value::Int(v) => v.to_string(),
169 Value::Float(v) => v.to_string(),
170 Value::Decimal(v) => v.to_sql_string(),
171 Value::Str(s) => s.clone(),
172 Value::FixedChar(s) => s.trim_end_matches(' ').to_string(),
173 Value::Temporal(t) => t.to_sql_string(),
174 Value::Json(text) | Value::JsonB(text) => text.clone(),
175 Value::Array(array) => crate::expr::array_value_to_string(array),
176 Value::Bytes(b) => {
177 use std::fmt::Write as _;
178 let mut out = String::with_capacity(2 + b.len() * 2);
179 out.push_str("\\x");
180 for byte in b {
181 let _ = write!(out, "{byte:02x}");
182 }
183 out
184 }
185 Value::List(items) => {
186 let inner = items.iter().map(raise_text).collect::<Vec<_>>().join(",");
187 format!("{{{inner}}}")
188 }
189 Value::Row(items) => {
190 let inner = items.iter().map(raise_text).collect::<Vec<_>>().join(",");
191 format!("({inner})")
192 }
193 Value::Record(fields) => {
194 let inner = fields
195 .iter()
196 .map(|(_, value)| raise_text(value))
197 .collect::<Vec<_>>()
198 .join(",");
199 format!("({inner})")
200 }
201 Value::Map(map) => serde_json::to_string(map).unwrap_or_else(|_| format!("{map:?}")),
202 }
203}
204
205#[cfg(test)]
206mod tests;