1use pg_query::NodeEnum;
14use pg_query::protobuf::{CreateFunctionStmt, FunctionParameterMode};
15
16use crate::identifier::Identifier;
17use crate::ir::function::{
18 ArgMode, Function, FunctionArg, FunctionLanguage, NormalizedArgTypes, ParallelSafety,
19 ReturnType, SecurityMode, TableColumn, Volatility,
20};
21use crate::ir::procedure::Procedure;
22use crate::parse::builder::plpgsql;
23use crate::parse::builder::shared;
24use crate::parse::error::{ParseError, SourceLocation};
25use crate::parse::normalize_body::NormalizedBody;
26use crate::parse::normalize_expr;
27
28#[derive(Debug)]
30pub enum Routine {
31 Function(Function),
33 Procedure(Procedure),
35}
36
37#[allow(clippy::too_many_lines)] pub(crate) fn build_function_or_procedure(
48 stmt: &CreateFunctionStmt,
49 default_schema: Option<&Identifier>,
50 location: &SourceLocation,
51) -> Result<Routine, ParseError> {
52 let qname = shared::qname_from_string_list(&stmt.funcname, default_schema, location)?;
53 let is_procedure = stmt.is_procedure;
54
55 let mut args: Vec<FunctionArg> = Vec::new();
57 let mut table_columns: Vec<TableColumn> = Vec::new();
58
59 for param_node in &stmt.parameters {
60 let Some(NodeEnum::FunctionParameter(p)) = param_node.node.as_ref() else {
61 return Err(ParseError::Structural {
62 location: location.clone(),
63 message: format!("CREATE FUNCTION/PROCEDURE {qname}: unexpected parameter node"),
64 });
65 };
66
67 let raw_mode =
68 FunctionParameterMode::try_from(p.mode).unwrap_or(FunctionParameterMode::Undefined);
69
70 if raw_mode == FunctionParameterMode::FuncParamTable {
72 let tn = p.arg_type.as_ref().ok_or_else(|| ParseError::Structural {
73 location: location.clone(),
74 message: format!("{qname}: TABLE column missing type"),
75 })?;
76 let ty = shared::type_name_to_column_type(tn, location)?;
77 let name = if p.name.is_empty() {
78 return Err(ParseError::Structural {
79 location: location.clone(),
80 message: format!("{qname}: TABLE column missing name"),
81 });
82 } else {
83 shared::ident(&p.name, location)?
84 };
85 table_columns.push(TableColumn { name, ty });
86 continue;
87 }
88
89 let mode = match raw_mode {
90 FunctionParameterMode::FuncParamIn
91 | FunctionParameterMode::FuncParamDefault
92 | FunctionParameterMode::Undefined => ArgMode::In,
93 FunctionParameterMode::FuncParamOut => ArgMode::Out,
94 FunctionParameterMode::FuncParamInout => ArgMode::InOut,
95 FunctionParameterMode::FuncParamVariadic => ArgMode::Variadic,
96 FunctionParameterMode::FuncParamTable => unreachable!("handled above"),
97 };
98
99 let tn = p.arg_type.as_ref().ok_or_else(|| ParseError::Structural {
100 location: location.clone(),
101 message: format!("{qname}: parameter missing type"),
102 })?;
103 let ty = shared::type_name_to_column_type(tn, location)?;
104
105 let name = if p.name.is_empty() {
106 None
107 } else {
108 Some(shared::ident(&p.name, location)?)
109 };
110
111 let default = if let Some(defexpr_boxed) = p.defexpr.as_ref() {
113 let node_enum = defexpr_boxed
114 .node
115 .as_ref()
116 .ok_or_else(|| ParseError::Structural {
117 location: location.clone(),
118 message: format!("{qname}: argument default expression is empty"),
119 })?;
120 Some(normalize_expr::from_pg_node(
121 node_enum,
122 Some(&ty),
123 location,
124 )?)
125 } else {
126 None
127 };
128
129 args.push(FunctionArg {
130 name,
131 mode,
132 ty,
133 default,
134 });
135 }
136
137 let mut language: Option<FunctionLanguage> = None;
139 let mut volatility: Option<Volatility> = None;
140 let mut strict = false;
141 let mut security: Option<SecurityMode> = None;
142 let mut parallel: Option<ParallelSafety> = None;
143 let mut leakproof = false;
144 let mut cost: Option<f32> = None;
145 let mut rows: Option<f32> = None;
146 let mut body_text: Option<String> = None;
147
148 for opt_node in &stmt.options {
149 let Some(NodeEnum::DefElem(de)) = opt_node.node.as_ref() else {
150 return Err(ParseError::Structural {
151 location: location.clone(),
152 message: format!("{qname}: unexpected option node"),
153 });
154 };
155
156 match de.defname.as_str() {
157 "language" => {
158 let lang_str =
159 shared::def_elem_string(de).ok_or_else(|| ParseError::Structural {
160 location: location.clone(),
161 message: format!("{qname}: LANGUAGE option missing value"),
162 })?;
163 language = Some(match lang_str.to_lowercase().as_str() {
164 "sql" => FunctionLanguage::Sql,
165 "plpgsql" => FunctionLanguage::PlPgSql,
166 other => {
167 return Err(ParseError::Structural {
168 location: location.clone(),
169 message: format!(
170 "{qname}: unsupported language {other:?} \
171 (pgevolve v0.2 supports sql and plpgsql)"
172 ),
173 });
174 }
175 });
176 }
177 "volatility" => {
178 if is_procedure {
179 return Err(ParseError::Structural {
180 location: location.clone(),
181 message: format!(
182 "{qname}: VOLATILE/STABLE/IMMUTABLE is not valid on procedures"
183 ),
184 });
185 }
186 let v_str = shared::def_elem_string(de).ok_or_else(|| ParseError::Structural {
187 location: location.clone(),
188 message: format!("{qname}: volatility option missing value"),
189 })?;
190 volatility = Some(match v_str.to_lowercase().as_str() {
191 "immutable" => Volatility::Immutable,
192 "stable" => Volatility::Stable,
193 "volatile" => Volatility::Volatile,
194 other => {
195 return Err(ParseError::Structural {
196 location: location.clone(),
197 message: format!("{qname}: unknown volatility {other:?}"),
198 });
199 }
200 });
201 }
202 "strict" => {
203 if is_procedure {
204 return Err(ParseError::Structural {
205 location: location.clone(),
206 message: format!("{qname}: STRICT is not valid on procedures"),
207 });
208 }
209 strict = bool_from_def_elem(de);
210 }
211 "security" => {
212 security = Some(match de.arg.as_ref().and_then(|a| a.node.as_ref()) {
216 Some(NodeEnum::Boolean(b)) => {
217 if b.boolval {
218 SecurityMode::Definer
219 } else {
220 SecurityMode::Invoker
221 }
222 }
223 Some(NodeEnum::Integer(i)) => {
224 if i.ival != 0 {
225 SecurityMode::Definer
226 } else {
227 SecurityMode::Invoker
228 }
229 }
230 Some(NodeEnum::String(s)) => match s.sval.to_lowercase().as_str() {
231 "invoker" => SecurityMode::Invoker,
232 "definer" => SecurityMode::Definer,
233 other => {
234 return Err(ParseError::Structural {
235 location: location.clone(),
236 message: format!("{qname}: unknown security mode {other:?}"),
237 });
238 }
239 },
240 None => SecurityMode::Invoker, _ => {
242 return Err(ParseError::Structural {
243 location: location.clone(),
244 message: format!("{qname}: SECURITY option has unexpected value"),
245 });
246 }
247 });
248 }
249 "parallel" => {
250 if is_procedure {
251 return Err(ParseError::Structural {
252 location: location.clone(),
253 message: format!("{qname}: PARALLEL is not valid on procedures"),
254 });
255 }
256 let p_str = shared::def_elem_string(de).ok_or_else(|| ParseError::Structural {
257 location: location.clone(),
258 message: format!("{qname}: PARALLEL option missing value"),
259 })?;
260 parallel = Some(match p_str.to_lowercase().as_str() {
261 "safe" => ParallelSafety::Safe,
262 "restricted" => ParallelSafety::Restricted,
263 "unsafe" => ParallelSafety::Unsafe,
264 other => {
265 return Err(ParseError::Structural {
266 location: location.clone(),
267 message: format!("{qname}: unknown parallel safety {other:?}"),
268 });
269 }
270 });
271 }
272 "leakproof" => {
273 if is_procedure {
274 return Err(ParseError::Structural {
275 location: location.clone(),
276 message: format!("{qname}: LEAKPROOF is not valid on procedures"),
277 });
278 }
279 leakproof = bool_from_def_elem(de);
280 }
281 "cost" => {
282 if is_procedure {
283 return Err(ParseError::Structural {
284 location: location.clone(),
285 message: format!("{qname}: COST is not valid on procedures"),
286 });
287 }
288 cost = float_from_def_elem(de);
291 }
292 "rows" => {
293 if is_procedure {
294 return Err(ParseError::Structural {
295 location: location.clone(),
296 message: format!("{qname}: ROWS is not valid on procedures"),
297 });
298 }
299 rows = float_from_def_elem(de);
302 }
303 "as" => {
304 body_text = Some(shared::def_elem_string(de).unwrap_or_default());
307 }
308 other => {
309 return Err(ParseError::Structural {
310 location: location.clone(),
311 message: format!("{qname}: unsupported function option {other:?}"),
312 });
313 }
314 }
315 }
316
317 let lang = language.unwrap_or(FunctionLanguage::Sql);
318
319 let raw_body = body_text.as_deref().unwrap_or("").trim().to_string();
323 let (body, body_deps, commits_in_body) = if raw_body.is_empty() {
324 (NormalizedBody::empty(), vec![], false)
325 } else {
326 plpgsql::parse_routine_body(&raw_body, lang, &qname, location)?
327 };
328
329 if is_procedure {
330 if stmt.return_type.is_some() {
332 return Err(ParseError::Structural {
333 location: location.clone(),
334 message: format!("{qname}: procedures cannot have a RETURNS clause"),
335 });
336 }
337
338 let security = security.unwrap_or(SecurityMode::Invoker);
339
340 return Ok(Routine::Procedure(Procedure {
341 qname,
342 args,
343 language: lang,
344 body,
345 body_dependencies: body_deps,
346 security,
347 commits_in_body,
348 comment: None,
349 owner: None,
350 grants: vec![],
351 }));
352 }
353
354 let return_type = if !table_columns.is_empty() {
356 ReturnType::Table {
358 columns: table_columns,
359 }
360 } else if let Some(tn) = stmt.return_type.as_ref() {
361 let type_str =
362 shared::render_type_name_to_string(tn).ok_or_else(|| ParseError::Structural {
363 location: location.clone(),
364 message: format!("{qname}: could not stringify return type"),
365 })?;
366 match type_str.to_lowercase().as_str() {
367 "trigger" => ReturnType::Trigger,
368 "event_trigger" => ReturnType::EventTrigger,
369 "void" => ReturnType::Void,
370 _ => {
371 let ty = shared::type_name_to_column_type(tn, location)?;
372 if tn.setof {
373 ReturnType::SetOf { ty }
374 } else {
375 ReturnType::Scalar { ty }
376 }
377 }
378 }
379 } else {
380 return Err(ParseError::Structural {
381 location: location.clone(),
382 message: format!("{qname}: function is missing a RETURNS clause"),
383 });
384 };
385
386 let arg_types_normalized = NormalizedArgTypes::from_args(&args);
387
388 Ok(Routine::Function(Function {
389 qname,
390 args,
391 arg_types_normalized,
392 return_type,
393 language: lang,
394 body,
395 body_dependencies: body_deps,
396 volatility: volatility.unwrap_or(Volatility::Volatile),
397 strict,
398 security: security.unwrap_or(SecurityMode::Invoker),
399 parallel: parallel.unwrap_or(ParallelSafety::Unsafe),
400 leakproof,
401 cost,
402 rows,
403 comment: None,
404 owner: None,
405 grants: vec![],
406 }))
407}
408
409fn bool_from_def_elem(de: &pg_query::protobuf::DefElem) -> bool {
412 let Some(arg) = de.arg.as_ref() else {
413 return true; };
415 match arg.node.as_ref() {
416 Some(NodeEnum::Integer(i)) => i.ival != 0,
417 _ => true,
418 }
419}
420
421fn float_from_def_elem(de: &pg_query::protobuf::DefElem) -> Option<f32> {
423 let arg = de.arg.as_ref()?;
424 match arg.node.as_ref()? {
425 NodeEnum::Float(f) => f.fval.parse::<f32>().ok(),
426 #[allow(clippy::cast_precision_loss)]
427 NodeEnum::Integer(i) => Some(i.ival as f32),
428 _ => None,
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use std::path::PathBuf;
436
437 use pg_query::NodeEnum as PgNodeEnum;
438
439 fn loc() -> SourceLocation {
440 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
441 }
442
443 fn parse_function(sql: &str) -> CreateFunctionStmt {
444 let parsed = pg_query::parse(sql).expect("parses");
445 let node = parsed
446 .protobuf
447 .stmts
448 .into_iter()
449 .next()
450 .and_then(|r| r.stmt)
451 .and_then(|n| n.node)
452 .expect("stmt");
453 let PgNodeEnum::CreateFunctionStmt(boxed) = node else {
454 panic!("expected CreateFunctionStmt, got: {node:?}");
455 };
456 *boxed
457 }
458
459 fn build_fn(sql: &str) -> Function {
460 let stmt = parse_function(sql);
461 match build_function_or_procedure(&stmt, None, &loc()).expect("build") {
462 Routine::Function(f) => f,
463 Routine::Procedure(_) => panic!("expected Function"),
464 }
465 }
466
467 fn build_proc(sql: &str) -> Procedure {
468 let stmt = parse_function(sql);
469 match build_function_or_procedure(&stmt, None, &loc()).expect("build") {
470 Routine::Procedure(p) => p,
471 Routine::Function(_) => panic!("expected Procedure"),
472 }
473 }
474
475 #[test]
476 fn simple_sql_function() {
477 let f = build_fn(
478 "CREATE FUNCTION app.double(x integer) RETURNS integer \
479 LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT x * 2 $$;",
480 );
481 assert_eq!(f.qname.to_string(), "app.double");
482 assert_eq!(f.args.len(), 1);
483 assert_eq!(
484 f.args[0]
485 .name
486 .as_ref()
487 .map(crate::identifier::Identifier::as_str),
488 Some("x")
489 );
490 assert_eq!(f.args[0].mode, ArgMode::In);
491 assert!(matches!(
492 f.return_type,
493 ReturnType::Scalar {
494 ty: crate::ir::column_type::ColumnType::Integer
495 }
496 ));
497 assert_eq!(f.language, FunctionLanguage::Sql);
498 assert!(matches!(f.volatility, Volatility::Immutable));
499 assert!(f.strict);
500 }
501
502 #[test]
503 fn plpgsql_function_body_parsed() {
504 let f = build_fn(
505 "CREATE FUNCTION app.greet(name text) RETURNS text \
506 LANGUAGE plpgsql AS $$ BEGIN RETURN 'hello'; END $$;",
507 );
508 assert_eq!(f.qname.to_string(), "app.greet");
509 assert_eq!(f.language, FunctionLanguage::PlPgSql);
510 assert!(
512 !f.body.canonical_text().is_empty(),
513 "PL/pgSQL body should be canonicalized, got empty string"
514 );
515 }
516
517 #[test]
518 fn unqualified_uses_default_schema() {
519 let stmt = parse_function(
520 "CREATE FUNCTION double(x integer) RETURNS integer \
521 LANGUAGE sql AS $$ SELECT x $$;",
522 );
523 let app = Identifier::from_unquoted("app").unwrap();
524 let Routine::Function(f) = build_function_or_procedure(&stmt, Some(&app), &loc()).unwrap()
525 else {
526 panic!()
527 };
528 assert_eq!(f.qname.to_string(), "app.double");
529 }
530
531 #[test]
532 fn unsupported_language_rejected() {
533 let stmt = parse_function("CREATE FUNCTION app.f() RETURNS void LANGUAGE plperl AS $$ $$;");
534 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
535 let msg = match &err {
536 ParseError::Structural { message, .. } => message.clone(),
537 other => panic!("expected Structural, got {other:?}"),
538 };
539 assert!(msg.contains("plperl"), "should name bad language: {msg}");
540 }
541
542 #[test]
543 fn procedure_builds() {
544 let p = build_proc(
545 "CREATE PROCEDURE app.do_work() \
546 LANGUAGE plpgsql AS $$ BEGIN NULL; END $$;",
547 );
548 assert_eq!(p.qname.to_string(), "app.do_work");
549 assert!(p.args.is_empty());
550 assert_eq!(p.language, FunctionLanguage::PlPgSql);
551 }
552
553 #[test]
554 fn procedure_rejects_volatility() {
555 let stmt = parse_function(
556 "CREATE PROCEDURE app.p() LANGUAGE plpgsql VOLATILE AS $$ BEGIN NULL; END $$;",
557 );
558 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
559 let msg = match &err {
560 ParseError::Structural { message, .. } => message.clone(),
561 other => panic!("expected Structural, got {other:?}"),
562 };
563 assert!(
564 msg.contains("VOLATILE") || msg.contains("STABLE") || msg.contains("IMMUTABLE"),
565 "msg: {msg}"
566 );
567 }
568
569 #[test]
570 fn procedure_rejects_return_type() {
571 use pg_query::NodeEnum;
576 use pg_query::protobuf::{CreateFunctionStmt, Node, TypeName};
577
578 let funcname = vec![
580 Node {
581 node: Some(NodeEnum::String(pg_query::protobuf::String {
582 sval: "app".into(),
583 })),
584 },
585 Node {
586 node: Some(NodeEnum::String(pg_query::protobuf::String {
587 sval: "proc".into(),
588 })),
589 },
590 ];
591 let return_type = TypeName {
593 names: vec![Node {
594 node: Some(NodeEnum::String(pg_query::protobuf::String {
595 sval: "integer".into(),
596 })),
597 }],
598 type_oid: 0,
599 setof: false,
600 pct_type: false,
601 typmods: vec![],
602 typemod: -1,
603 array_bounds: vec![],
604 location: 0,
605 };
606
607 let stmt = CreateFunctionStmt {
608 is_procedure: true,
609 replace: false,
610 funcname,
611 parameters: vec![],
612 return_type: Some(return_type),
613 options: vec![],
614 sql_body: None,
615 };
616
617 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
618 let msg = match &err {
619 ParseError::Structural { message, .. } => message.clone(),
620 other => panic!("expected Structural, got {other:?}"),
621 };
622 let lower = msg.to_lowercase();
623 assert!(
624 lower.contains("return") || lower.contains("returns"),
625 "expected a 'returns/return-type' rejection message; got: {msg}",
626 );
627 }
628
629 #[test]
630 fn function_with_default_arg() {
631 let f = build_fn(
632 "CREATE FUNCTION app.greet(name text DEFAULT 'world') RETURNS text \
633 LANGUAGE sql AS $$ SELECT 'hello ' || name $$;",
634 );
635 assert_eq!(f.args.len(), 1);
636 assert!(f.args[0].default.is_some(), "should have a default");
637 }
638
639 #[test]
640 fn security_definer() {
641 let f = build_fn(
642 "CREATE FUNCTION app.f() RETURNS void LANGUAGE sql SECURITY DEFINER \
643 AS $$ SELECT NULL $$;",
644 );
645 assert!(matches!(f.security, SecurityMode::Definer));
646 }
647}