1use super::{
10 condition_sqlstate, ensure_single_tag, expect_tag, json_bool_or_false, json_kind,
11 json_optional_i64, json_usize_or_zero, lower_block, lower_expr, lower_full_statement,
12 normalize_plpgsql_type, optional_array, require, require_nonempty_str,
13 validate_assignable_datum, CreateFunction, FunctionBody, FunctionParamMode, FunctionReturns,
14 JSONValue, PLpgSQLCursor, PLpgSQLDatum, PLpgSQLFunction, PLpgSQLRowField, PLpgSQLVar, Result,
15 RoutineColumnTypeReference, SQLError,
16};
17
18pub fn parse_function(def: &CreateFunction) -> Result<PLpgSQLFunction> {
19 let FunctionBody::Source(body) = &def.body else {
20 return Err(SQLError::Internal(
21 "PL/pgSQL parser invoked on a SQL-standard body".into(),
22 ));
23 };
24 let text = synthesize_create_text(def, body);
25 parse_plpgsql_text(&text)
26}
27
28pub fn parse_do_block(body: &str) -> Result<PLpgSQLFunction> {
30 let tag = fresh_dollar_tag(body);
31 let text = format!("DO {tag}{body}{tag} LANGUAGE plpgsql;");
32 parse_plpgsql_text(&text)
33}
34
35pub(super) fn synthesize_create_text(def: &CreateFunction, body: &str) -> String {
39 let mut sql = String::new();
40 sql.push_str(if def.is_procedure {
41 "CREATE PROCEDURE "
42 } else {
43 "CREATE FUNCTION "
44 });
45 sql.push_str("e_ident(&def.name));
46 sql.push('(');
47 let mut first = true;
48 for p in &def.params {
49 if matches!(p.mode, FunctionParamMode::Table) {
50 continue;
51 }
52 if !first {
53 sql.push_str(", ");
54 }
55 first = false;
56 match p.mode {
57 FunctionParamMode::Out => sql.push_str("OUT "),
58 FunctionParamMode::InOut => sql.push_str("INOUT "),
59 FunctionParamMode::Variadic => sql.push_str("VARIADIC "),
60 FunctionParamMode::In | FunctionParamMode::Table => {}
61 }
62 if !p.name.is_empty() {
63 sql.push_str("e_ident(&p.name));
64 sql.push(' ');
65 }
66 sql.push_str(&p.type_name);
67 }
68 sql.push(')');
69 match &def.returns {
70 FunctionReturns::None => {}
71 FunctionReturns::Scalar { type_name } => {
72 sql.push_str(" RETURNS ");
73 sql.push_str(type_name);
74 }
75 FunctionReturns::SetOf { type_name } => {
76 sql.push_str(" RETURNS SETOF ");
77 sql.push_str(type_name);
78 }
79 FunctionReturns::Table => {
80 sql.push_str(" RETURNS TABLE(");
81 let mut first_col = true;
82 for p in &def.params {
83 if !matches!(p.mode, FunctionParamMode::Table) {
84 continue;
85 }
86 if !first_col {
87 sql.push_str(", ");
88 }
89 first_col = false;
90 sql.push_str("e_ident(&p.name));
91 sql.push(' ');
92 sql.push_str(&p.type_name);
93 }
94 sql.push(')');
95 }
96 }
97 let tag = fresh_dollar_tag(body);
98 sql.push_str(" AS ");
99 sql.push_str(&tag);
100 sql.push_str(body);
101 sql.push_str(&tag);
102 sql.push_str(" LANGUAGE plpgsql;");
103 sql
104}
105
106pub(super) fn quote_ident(name: &str) -> String {
107 format!("\"{}\"", name.replace('"', "\"\""))
108}
109
110pub(super) fn fresh_dollar_tag(body: &str) -> String {
112 let mut n = 0usize;
113 loop {
114 let tag = if n == 0 {
115 "$$".to_string()
116 } else {
117 format!("$plpgsql{n}$")
118 };
119 if !body.contains(&tag) {
120 return tag;
121 }
122 n += 1;
123 }
124}
125
126pub(super) fn parse_plpgsql_text(text: &str) -> Result<PLpgSQLFunction> {
127 let json = pg_query::parse_plpgsql(text)?;
128 let functions = json
129 .as_array()
130 .ok_or_else(|| SQLError::Internal("PL/pgSQL parse returned no function list".into()))?;
131 if functions.len() != 1 {
132 return Err(SQLError::Internal(format!(
133 "PL/pgSQL parse returned {} functions; expected exactly one",
134 functions.len()
135 )));
136 }
137 let function = expect_tag(&functions[0], "PLpgSQL_function", "parsed function")?;
138 lower_function(function)
139}
140
141pub(super) fn lower_function(function: &JSONValue) -> Result<PLpgSQLFunction> {
151 let raw_datums = function
152 .get("datums")
153 .and_then(JSONValue::as_array)
154 .ok_or_else(|| SQLError::Internal("PL/pgSQL function without datums".into()))?;
155 let mut datums = Vec::with_capacity(raw_datums.len());
156 for raw in raw_datums {
157 datums.push(lower_datum(raw)?);
158 }
159 validate_datums(&datums)?;
160 let trigger_datum = |field: &str, name: &str| -> Result<Option<usize>> {
161 let explicit = match json_optional_i64(function, field)? {
162 Some(index) if index >= 0 => {
163 let index = usize::try_from(index).map_err(|_| {
164 SQLError::Internal(format!(
165 "PL/pgSQL {field} {index} does not fit this platform"
166 ))
167 })?;
168 if index >= datums.len() {
169 return Err(SQLError::Internal(format!(
170 "PL/pgSQL {field} has out-of-range datum index {index}"
171 )));
172 }
173 Some(index)
174 }
175 Some(index) => {
176 return Err(SQLError::Internal(format!(
177 "PL/pgSQL {field} has invalid datum index {index}"
178 )))
179 }
180 None => None,
181 };
182 Ok(explicit.or_else(|| {
183 datums.iter().position(|datum| {
184 datum
185 .name()
186 .is_some_and(|datum_name| datum_name.eq_ignore_ascii_case(name))
187 })
188 }))
189 };
190 let new_datum = trigger_datum("new_varno", "new")?;
191 let old_datum = trigger_datum("old_varno", "old")?;
192 let found_datum = datums
193 .iter()
194 .position(|d| matches!(d, PLpgSQLDatum::Var(v) if v.name.eq_ignore_ascii_case("found")));
195 let raw_action = require(function, "action")?;
196 let action = expect_tag(raw_action, "PLpgSQL_stmt_block", "function body")?;
197 let action = lower_block(action, &datums)?;
198 Ok(PLpgSQLFunction {
199 datums,
200 action,
201 new_datum,
202 old_datum,
203 found_datum,
204 })
205}
206
207fn has_percent_type_suffix(type_name: &str) -> bool {
208 type_name
209 .get(type_name.len().saturating_sub("%type".len())..)
210 .is_some_and(|suffix| suffix.eq_ignore_ascii_case("%type"))
211}
212
213fn lower_percent_type_reference(
214 datatype: &JSONValue,
215 variable_name: &str,
216) -> Result<RoutineColumnTypeReference> {
217 let identifiers = require(datatype, "typname_identifiers")?
218 .as_array()
219 .ok_or_else(|| {
220 SQLError::Internal(format!(
221 "PL/pgSQL variable `{variable_name}` type metadata `typname_identifiers` must be an array"
222 ))
223 })?;
224 let identifiers = identifiers
225 .iter()
226 .enumerate()
227 .map(|(index, identifier)| match identifier.as_str() {
228 Some(identifier) if !identifier.is_empty() => Ok(identifier.to_string()),
229 _ => Err(SQLError::Internal(format!(
230 "PL/pgSQL variable `{variable_name}` type metadata identifier {index} must be a non-empty string"
231 ))),
232 })
233 .collect::<Result<Vec<_>>>()?;
234 match identifiers.as_slice() {
235 [relation, column] => Ok(RoutineColumnTypeReference::new(
236 None,
237 relation.clone(),
238 column.clone(),
239 )),
240 [schema, relation, column] => Ok(RoutineColumnTypeReference::new(
241 Some(schema.clone()),
242 relation.clone(),
243 column.clone(),
244 )),
245 _ => Err(SQLError::TypeMismatch(format!(
246 "PL/pgSQL variable `{variable_name}` %TYPE must identify a relation column"
247 ))),
248 }
249}
250
251pub(super) fn lower_datum(raw: &JSONValue) -> Result<PLpgSQLDatum> {
252 ensure_single_tag(raw, "datum")?;
253 if let Some(var) = raw.get("PLpgSQL_var") {
254 let name = require_nonempty_str(var, "refname", "variable datum")?;
255 let datatype = require(var, "datatype")?;
256 let datatype = expect_tag(datatype, "PLpgSQL_type", "variable datatype")?;
257 let type_name = normalize_plpgsql_type(&require_nonempty_str(
258 datatype,
259 "typname",
260 "variable datatype",
261 )?);
262 if type_name.is_empty() {
263 return Err(SQLError::Internal(format!(
264 "PL/pgSQL variable `{name}` has an empty normalized type"
265 )));
266 }
267 let type_reference = has_percent_type_suffix(&type_name)
268 .then(|| lower_percent_type_reference(datatype, &name))
269 .transpose()?;
270 let default = match var.get("default_val") {
271 Some(node) => Some(lower_expr(node)?),
272 None => None,
273 };
274 let cursor = if let Some(query) = var.get("cursor_explicit_expr") {
275 Some(PLpgSQLCursor {
276 query: lower_full_statement(query)?,
277 argument_row: match json_optional_i64(var, "cursor_explicit_argrow")? {
278 None | Some(-1) => None,
279 Some(index) if index >= 0 => Some(usize::try_from(index).map_err(|_| {
280 SQLError::Internal(format!(
281 "PL/pgSQL cursor `{name}` argument row {index} does not fit this platform"
282 ))
283 })?),
284 Some(index) => {
285 return Err(SQLError::Internal(format!(
286 "PL/pgSQL cursor `{name}` has invalid argument row {index}"
287 )));
288 }
289 },
290 })
291 } else {
292 if var.get("cursor_explicit_argrow").is_some() {
293 return Err(SQLError::Internal(format!(
294 "PL/pgSQL cursor variable `{name}` has arguments but no query"
295 )));
296 }
297 None
298 };
299 return Ok(PLpgSQLDatum::Var(Box::new(PLpgSQLVar {
300 name,
301 type_name,
302 type_reference,
303 default,
304 constant: json_bool_or_false(var, "isconst")?,
305 not_null: json_bool_or_false(var, "notnull")?,
306 cursor,
307 lineno: json_optional_i64(var, "lineno")?,
308 })));
309 }
310 if let Some(rec) = raw.get("PLpgSQL_rec") {
311 return Ok(PLpgSQLDatum::Rec {
312 name: require_nonempty_str(rec, "refname", "record datum")?,
313 });
314 }
315 if let Some(field) = raw.get("PLpgSQL_recfield") {
316 return Ok(PLpgSQLDatum::RecField {
317 field: require_nonempty_str(field, "fieldname", "record-field datum")?,
318 parent: json_usize_or_zero(field, "recparentno")?,
320 });
321 }
322 if let Some(row) = raw.get("PLpgSQL_row") {
323 return Ok(PLpgSQLDatum::Row {
324 fields: lower_row_fields(row)?,
325 });
326 }
327 Err(SQLError::Unsupported(format!(
328 "PL/pgSQL datum {}",
329 json_kind(raw)
330 )))
331}
332
333pub(super) fn lower_row_fields(row: &JSONValue) -> Result<Vec<PLpgSQLRowField>> {
334 let mut out = Vec::new();
335 if let Some(fields) = optional_array(row, "fields")? {
336 for f in fields {
337 out.push(PLpgSQLRowField {
340 name: require_nonempty_str(f, "name", "row target field")?,
341 varno: json_usize_or_zero(f, "varno")?,
342 });
343 }
344 }
345 Ok(out)
346}
347
348pub(super) fn validate_datums(datums: &[PLpgSQLDatum]) -> Result<()> {
349 for (idx, datum) in datums.iter().enumerate() {
350 match datum {
351 PLpgSQLDatum::RecField { parent, .. } => {
352 let Some(parent_datum) = datums.get(*parent) else {
353 return Err(SQLError::Internal(format!(
354 "PL/pgSQL record-field datum {idx} references missing parent datum {parent}"
355 )));
356 };
357 if !matches!(parent_datum, PLpgSQLDatum::Rec { .. }) {
358 return Err(SQLError::Internal(format!(
359 "PL/pgSQL record-field datum {idx} parent {parent} is not a record"
360 )));
361 }
362 }
363 PLpgSQLDatum::Row { fields } => {
364 if fields.is_empty() {
365 return Err(SQLError::Internal(format!(
366 "PL/pgSQL row datum {idx} has no fields"
367 )));
368 }
369 for field in fields {
370 validate_assignable_datum(datums, field.varno, "row target field")?;
371 }
372 }
373 PLpgSQLDatum::Var(var) => {
374 if let Some(cursor) = &var.cursor {
375 if var.type_name != "refcursor" {
376 return Err(SQLError::Internal(format!(
377 "PL/pgSQL bound cursor `{}` is not a refcursor datum",
378 var.name
379 )));
380 }
381 if let Some(argument_row) = cursor.argument_row {
382 if !matches!(datums.get(argument_row), Some(PLpgSQLDatum::Row { .. })) {
383 return Err(SQLError::Internal(format!(
384 "PL/pgSQL cursor `{}` references invalid argument row {argument_row}",
385 var.name
386 )));
387 }
388 }
389 }
390 }
391 PLpgSQLDatum::Rec { .. } => {}
392 }
393 }
394 Ok(())
395}
396
397pub(super) fn normalize_condition(value: String, allow_others: bool) -> Result<String> {
398 let lower = value.to_ascii_lowercase();
399 if allow_others && lower == "others" {
400 return Ok(lower);
401 }
402 if condition_sqlstate(&lower).is_some() {
403 return Ok(lower);
404 }
405 let upper = value.to_ascii_uppercase();
406 if upper.len() == 5
407 && upper
408 .bytes()
409 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
410 {
411 return Ok(upper);
412 }
413 Err(SQLError::Internal(format!(
414 "unrecognized PL/pgSQL exception condition `{value}`"
415 )))
416}