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