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 routine_message(error: &SQLError) -> String {
77 match error {
78 SQLError::Routine { message, .. } => message.clone(),
79 other => other.to_string(),
80 }
81}
82
83pub fn looks_like_sqlstate(text: &str) -> bool {
84 text.len() == 5 && text.bytes().all(|b| b.is_ascii_alphanumeric())
85}
86
87pub fn arm_matches(conditions: &[String], state: &str) -> Result<bool, SQLError> {
89 for condition in conditions {
90 if condition == "others" {
91 if state != "57014" && state != "P0004" {
94 return Ok(true);
95 }
96 continue;
97 }
98 let mut known_condition = false;
99 for mapped in condition_sqlstates(condition) {
100 known_condition = true;
101 if sqlstate_matches(mapped, state) {
102 return Ok(true);
103 }
104 }
105 if !known_condition {
106 if looks_like_sqlstate(condition) {
107 if sqlstate_matches(&condition.to_ascii_uppercase(), state) {
108 return Ok(true);
109 }
110 } else {
111 return Err(SQLError::Internal(format!(
112 "unrecognized PL/pgSQL exception condition `{condition}`"
113 )));
114 }
115 }
116 }
117 Ok(false)
118}
119
120pub fn sqlstate_matches(condition: &str, state: &str) -> bool {
121 condition == state || (condition.ends_with("000") && state.get(..2) == condition.get(..2))
122}
123
124pub fn format_raise_message(format: &str, args: &[Value]) -> Result<String, SQLError> {
126 let mut out = String::with_capacity(format.len() + 16);
127 let mut chars = format.chars().peekable();
128 let mut next_arg = 0usize;
129 while let Some(c) = chars.next() {
130 if c != '%' {
131 out.push(c);
132 continue;
133 }
134 if chars.peek() == Some(&'%') {
135 chars.next();
136 out.push('%');
137 continue;
138 }
139 let Some(value) = args.get(next_arg) else {
140 return Err(SQLError::Routine {
141 sqlstate: "42601".into(),
142 message: "too few parameters specified for RAISE".into(),
143 });
144 };
145 next_arg += 1;
146 out.push_str(&raise_text(value)?);
147 }
148 if next_arg < args.len() {
149 return Err(SQLError::Routine {
150 sqlstate: "42601".into(),
151 message: "too many parameters specified for RAISE".into(),
152 });
153 }
154 Ok(out)
155}
156
157pub fn raise_text(value: &Value) -> Result<String, SQLError> {
160 Ok(match value {
161 Value::Null => "<NULL>".into(),
162 Value::Void => String::new(),
163 Value::Bool(b) => (if *b { "t" } else { "f" }).into(),
164 Value::Int(v) => v.to_string(),
165 Value::Float(v) => v.to_string(),
166 Value::Decimal(v) => v.to_sql_string(),
167 Value::Str(s) => s.clone(),
168 Value::FixedChar(s) => s.trim_end_matches(' ').to_string(),
169 Value::Temporal(t) => t.to_sql_string(),
170 Value::Json(text) | Value::JsonB(text) => text.clone(),
171 Value::Array(_) | Value::LegacyVector(_) => crate::expr::value_to_string(value)?,
172 Value::Enum(value) => return Err(crate::expr::catalog_output_required(value)),
173 Value::Bytes(b) => {
174 use std::fmt::Write as _;
175 let mut out = String::with_capacity(2 + b.len() * 2);
176 out.push_str("\\x");
177 for byte in b {
178 let _ = write!(out, "{byte:02x}");
179 }
180 out
181 }
182 Value::List(items) => {
183 let inner = items
184 .iter()
185 .map(raise_text)
186 .collect::<Result<Vec<_>, _>>()?
187 .join(",");
188 format!("{{{inner}}}")
189 }
190 Value::Row(items) => {
191 let inner = items
192 .iter()
193 .map(raise_text)
194 .collect::<Result<Vec<_>, _>>()?
195 .join(",");
196 format!("({inner})")
197 }
198 Value::Record(fields) => {
199 let inner = fields
200 .iter()
201 .map(|(_, value)| raise_text(value))
202 .collect::<Result<Vec<_>, _>>()?
203 .join(",");
204 format!("({inner})")
205 }
206 Value::Map(map) => serde_json::to_string(map).unwrap_or_else(|_| format!("{map:?}")),
207 })
208}
209
210#[cfg(test)]
211mod tests;