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 = string_from_def_elem(de).ok_or_else(|| ParseError::Structural {
159 location: location.clone(),
160 message: format!("{qname}: LANGUAGE option missing value"),
161 })?;
162 language = Some(match lang_str.to_lowercase().as_str() {
163 "sql" => FunctionLanguage::Sql,
164 "plpgsql" => FunctionLanguage::PlPgSql,
165 other => {
166 return Err(ParseError::Structural {
167 location: location.clone(),
168 message: format!(
169 "{qname}: unsupported language {other:?} \
170 (pgevolve v0.2 supports sql and plpgsql)"
171 ),
172 });
173 }
174 });
175 }
176 "volatility" => {
177 if is_procedure {
178 return Err(ParseError::Structural {
179 location: location.clone(),
180 message: format!(
181 "{qname}: VOLATILE/STABLE/IMMUTABLE is not valid on procedures"
182 ),
183 });
184 }
185 let v_str = string_from_def_elem(de).ok_or_else(|| ParseError::Structural {
186 location: location.clone(),
187 message: format!("{qname}: volatility option missing value"),
188 })?;
189 volatility = Some(match v_str.to_lowercase().as_str() {
190 "immutable" => Volatility::Immutable,
191 "stable" => Volatility::Stable,
192 "volatile" => Volatility::Volatile,
193 other => {
194 return Err(ParseError::Structural {
195 location: location.clone(),
196 message: format!("{qname}: unknown volatility {other:?}"),
197 });
198 }
199 });
200 }
201 "strict" => {
202 if is_procedure {
203 return Err(ParseError::Structural {
204 location: location.clone(),
205 message: format!("{qname}: STRICT is not valid on procedures"),
206 });
207 }
208 strict = bool_from_def_elem(de);
209 }
210 "security" => {
211 security = Some(match de.arg.as_ref().and_then(|a| a.node.as_ref()) {
215 Some(NodeEnum::Boolean(b)) => {
216 if b.boolval {
217 SecurityMode::Definer
218 } else {
219 SecurityMode::Invoker
220 }
221 }
222 Some(NodeEnum::Integer(i)) => {
223 if i.ival != 0 {
224 SecurityMode::Definer
225 } else {
226 SecurityMode::Invoker
227 }
228 }
229 Some(NodeEnum::String(s)) => match s.sval.to_lowercase().as_str() {
230 "invoker" => SecurityMode::Invoker,
231 "definer" => SecurityMode::Definer,
232 other => {
233 return Err(ParseError::Structural {
234 location: location.clone(),
235 message: format!("{qname}: unknown security mode {other:?}"),
236 });
237 }
238 },
239 None => SecurityMode::Invoker, _ => {
241 return Err(ParseError::Structural {
242 location: location.clone(),
243 message: format!("{qname}: SECURITY option has unexpected value"),
244 });
245 }
246 });
247 }
248 "parallel" => {
249 if is_procedure {
250 return Err(ParseError::Structural {
251 location: location.clone(),
252 message: format!("{qname}: PARALLEL is not valid on procedures"),
253 });
254 }
255 let p_str = string_from_def_elem(de).ok_or_else(|| ParseError::Structural {
256 location: location.clone(),
257 message: format!("{qname}: PARALLEL option missing value"),
258 })?;
259 parallel = Some(match p_str.to_lowercase().as_str() {
260 "safe" => ParallelSafety::Safe,
261 "restricted" => ParallelSafety::Restricted,
262 "unsafe" => ParallelSafety::Unsafe,
263 other => {
264 return Err(ParseError::Structural {
265 location: location.clone(),
266 message: format!("{qname}: unknown parallel safety {other:?}"),
267 });
268 }
269 });
270 }
271 "leakproof" => {
272 if is_procedure {
273 return Err(ParseError::Structural {
274 location: location.clone(),
275 message: format!("{qname}: LEAKPROOF is not valid on procedures"),
276 });
277 }
278 leakproof = bool_from_def_elem(de);
279 }
280 "cost" => {
281 if is_procedure {
282 return Err(ParseError::Structural {
283 location: location.clone(),
284 message: format!("{qname}: COST is not valid on procedures"),
285 });
286 }
287 cost = float_from_def_elem(de);
290 }
291 "rows" => {
292 if is_procedure {
293 return Err(ParseError::Structural {
294 location: location.clone(),
295 message: format!("{qname}: ROWS is not valid on procedures"),
296 });
297 }
298 rows = float_from_def_elem(de);
301 }
302 "as" => {
303 body_text = Some(string_from_def_elem(de).unwrap_or_default());
306 }
307 other => {
308 return Err(ParseError::Structural {
309 location: location.clone(),
310 message: format!("{qname}: unsupported function option {other:?}"),
311 });
312 }
313 }
314 }
315
316 let lang = language.unwrap_or(FunctionLanguage::Sql);
317
318 let raw_body = body_text.as_deref().unwrap_or("").trim().to_string();
322 let (body, body_deps, commits_in_body) = if raw_body.is_empty() {
323 (NormalizedBody::empty(), vec![], false)
324 } else {
325 plpgsql::parse_routine_body(&raw_body, lang, &qname, location)?
326 };
327
328 if is_procedure {
329 if stmt.return_type.is_some() {
331 return Err(ParseError::Structural {
332 location: location.clone(),
333 message: format!("{qname}: procedures cannot have a RETURNS clause"),
334 });
335 }
336
337 let security = security.unwrap_or(SecurityMode::Invoker);
338
339 return Ok(Routine::Procedure(Procedure {
340 qname,
341 args,
342 language: lang,
343 body,
344 body_dependencies: body_deps,
345 security,
346 commits_in_body,
347 comment: None,
348 owner: None,
349 grants: vec![],
350 }));
351 }
352
353 let return_type = if !table_columns.is_empty() {
355 ReturnType::Table {
357 columns: table_columns,
358 }
359 } else if let Some(tn) = stmt.return_type.as_ref() {
360 let type_str =
361 shared::render_type_name_to_string(tn).ok_or_else(|| ParseError::Structural {
362 location: location.clone(),
363 message: format!("{qname}: could not stringify return type"),
364 })?;
365 match type_str.to_lowercase().as_str() {
366 "trigger" => ReturnType::Trigger,
367 "event_trigger" => ReturnType::EventTrigger,
368 "void" => ReturnType::Void,
369 _ => {
370 let ty = shared::type_name_to_column_type(tn, location)?;
371 if tn.setof {
372 ReturnType::SetOf { ty }
373 } else {
374 ReturnType::Scalar { ty }
375 }
376 }
377 }
378 } else {
379 return Err(ParseError::Structural {
380 location: location.clone(),
381 message: format!("{qname}: function is missing a RETURNS clause"),
382 });
383 };
384
385 let arg_types_normalized = NormalizedArgTypes::from_args(&args);
386
387 Ok(Routine::Function(Function {
388 qname,
389 args,
390 arg_types_normalized,
391 return_type,
392 language: lang,
393 body,
394 body_dependencies: body_deps,
395 volatility: volatility.unwrap_or(Volatility::Volatile),
396 strict,
397 security: security.unwrap_or(SecurityMode::Invoker),
398 parallel: parallel.unwrap_or(ParallelSafety::Unsafe),
399 leakproof,
400 cost,
401 rows,
402 comment: None,
403 owner: None,
404 grants: vec![],
405 }))
406}
407
408fn string_from_def_elem(de: &pg_query::protobuf::DefElem) -> Option<String> {
415 let arg = de.arg.as_ref()?;
416 match arg.node.as_ref()? {
417 NodeEnum::String(s) => Some(s.sval.clone()),
418 NodeEnum::Integer(i) => Some(i.ival.to_string()),
419 NodeEnum::Float(f) => Some(f.fval.clone()),
420 NodeEnum::List(list) => list
423 .items
424 .first()
425 .and_then(|n| n.node.as_ref())
426 .and_then(|n| {
427 if let NodeEnum::String(s) = n {
428 Some(s.sval.clone())
429 } else {
430 None
431 }
432 }),
433 _ => None,
434 }
435}
436
437fn bool_from_def_elem(de: &pg_query::protobuf::DefElem) -> bool {
440 let Some(arg) = de.arg.as_ref() else {
441 return true; };
443 match arg.node.as_ref() {
444 Some(NodeEnum::Integer(i)) => i.ival != 0,
445 _ => true,
446 }
447}
448
449fn float_from_def_elem(de: &pg_query::protobuf::DefElem) -> Option<f32> {
451 let arg = de.arg.as_ref()?;
452 match arg.node.as_ref()? {
453 NodeEnum::Float(f) => f.fval.parse::<f32>().ok(),
454 #[allow(clippy::cast_precision_loss)]
455 NodeEnum::Integer(i) => Some(i.ival as f32),
456 _ => None,
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use std::path::PathBuf;
464
465 use pg_query::NodeEnum as PgNodeEnum;
466
467 fn loc() -> SourceLocation {
468 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
469 }
470
471 fn parse_function(sql: &str) -> CreateFunctionStmt {
472 let parsed = pg_query::parse(sql).expect("parses");
473 let node = parsed
474 .protobuf
475 .stmts
476 .into_iter()
477 .next()
478 .and_then(|r| r.stmt)
479 .and_then(|n| n.node)
480 .expect("stmt");
481 let PgNodeEnum::CreateFunctionStmt(boxed) = node else {
482 panic!("expected CreateFunctionStmt, got: {node:?}");
483 };
484 *boxed
485 }
486
487 fn build_fn(sql: &str) -> Function {
488 let stmt = parse_function(sql);
489 match build_function_or_procedure(&stmt, None, &loc()).expect("build") {
490 Routine::Function(f) => f,
491 Routine::Procedure(_) => panic!("expected Function"),
492 }
493 }
494
495 fn build_proc(sql: &str) -> Procedure {
496 let stmt = parse_function(sql);
497 match build_function_or_procedure(&stmt, None, &loc()).expect("build") {
498 Routine::Procedure(p) => p,
499 Routine::Function(_) => panic!("expected Procedure"),
500 }
501 }
502
503 #[test]
504 fn simple_sql_function() {
505 let f = build_fn(
506 "CREATE FUNCTION app.double(x integer) RETURNS integer \
507 LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT x * 2 $$;",
508 );
509 assert_eq!(f.qname.to_string(), "app.double");
510 assert_eq!(f.args.len(), 1);
511 assert_eq!(
512 f.args[0]
513 .name
514 .as_ref()
515 .map(crate::identifier::Identifier::as_str),
516 Some("x")
517 );
518 assert_eq!(f.args[0].mode, ArgMode::In);
519 assert!(matches!(
520 f.return_type,
521 ReturnType::Scalar {
522 ty: crate::ir::column_type::ColumnType::Integer
523 }
524 ));
525 assert_eq!(f.language, FunctionLanguage::Sql);
526 assert!(matches!(f.volatility, Volatility::Immutable));
527 assert!(f.strict);
528 }
529
530 #[test]
531 fn plpgsql_function_body_parsed() {
532 let f = build_fn(
533 "CREATE FUNCTION app.greet(name text) RETURNS text \
534 LANGUAGE plpgsql AS $$ BEGIN RETURN 'hello'; END $$;",
535 );
536 assert_eq!(f.qname.to_string(), "app.greet");
537 assert_eq!(f.language, FunctionLanguage::PlPgSql);
538 assert!(
540 !f.body.canonical_text().is_empty(),
541 "PL/pgSQL body should be canonicalized, got empty string"
542 );
543 }
544
545 #[test]
546 fn unqualified_uses_default_schema() {
547 let stmt = parse_function(
548 "CREATE FUNCTION double(x integer) RETURNS integer \
549 LANGUAGE sql AS $$ SELECT x $$;",
550 );
551 let app = Identifier::from_unquoted("app").unwrap();
552 let Routine::Function(f) = build_function_or_procedure(&stmt, Some(&app), &loc()).unwrap()
553 else {
554 panic!()
555 };
556 assert_eq!(f.qname.to_string(), "app.double");
557 }
558
559 #[test]
560 fn unsupported_language_rejected() {
561 let stmt = parse_function("CREATE FUNCTION app.f() RETURNS void LANGUAGE plperl AS $$ $$;");
562 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
563 let msg = match &err {
564 ParseError::Structural { message, .. } => message.clone(),
565 other => panic!("expected Structural, got {other:?}"),
566 };
567 assert!(msg.contains("plperl"), "should name bad language: {msg}");
568 }
569
570 #[test]
571 fn procedure_builds() {
572 let p = build_proc(
573 "CREATE PROCEDURE app.do_work() \
574 LANGUAGE plpgsql AS $$ BEGIN NULL; END $$;",
575 );
576 assert_eq!(p.qname.to_string(), "app.do_work");
577 assert!(p.args.is_empty());
578 assert_eq!(p.language, FunctionLanguage::PlPgSql);
579 }
580
581 #[test]
582 fn procedure_rejects_volatility() {
583 let stmt = parse_function(
584 "CREATE PROCEDURE app.p() LANGUAGE plpgsql VOLATILE AS $$ BEGIN NULL; END $$;",
585 );
586 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
587 let msg = match &err {
588 ParseError::Structural { message, .. } => message.clone(),
589 other => panic!("expected Structural, got {other:?}"),
590 };
591 assert!(
592 msg.contains("VOLATILE") || msg.contains("STABLE") || msg.contains("IMMUTABLE"),
593 "msg: {msg}"
594 );
595 }
596
597 #[test]
598 fn procedure_rejects_return_type() {
599 use pg_query::NodeEnum;
604 use pg_query::protobuf::{CreateFunctionStmt, Node, TypeName};
605
606 let funcname = vec![
608 Node {
609 node: Some(NodeEnum::String(pg_query::protobuf::String {
610 sval: "app".into(),
611 })),
612 },
613 Node {
614 node: Some(NodeEnum::String(pg_query::protobuf::String {
615 sval: "proc".into(),
616 })),
617 },
618 ];
619 let return_type = TypeName {
621 names: vec![Node {
622 node: Some(NodeEnum::String(pg_query::protobuf::String {
623 sval: "integer".into(),
624 })),
625 }],
626 type_oid: 0,
627 setof: false,
628 pct_type: false,
629 typmods: vec![],
630 typemod: -1,
631 array_bounds: vec![],
632 location: 0,
633 };
634
635 let stmt = CreateFunctionStmt {
636 is_procedure: true,
637 replace: false,
638 funcname,
639 parameters: vec![],
640 return_type: Some(return_type),
641 options: vec![],
642 sql_body: None,
643 };
644
645 let err = build_function_or_procedure(&stmt, None, &loc()).unwrap_err();
646 let msg = match &err {
647 ParseError::Structural { message, .. } => message.clone(),
648 other => panic!("expected Structural, got {other:?}"),
649 };
650 let lower = msg.to_lowercase();
651 assert!(
652 lower.contains("return") || lower.contains("returns"),
653 "expected a 'returns/return-type' rejection message; got: {msg}",
654 );
655 }
656
657 #[test]
658 fn function_with_default_arg() {
659 let f = build_fn(
660 "CREATE FUNCTION app.greet(name text DEFAULT 'world') RETURNS text \
661 LANGUAGE sql AS $$ SELECT 'hello ' || name $$;",
662 );
663 assert_eq!(f.args.len(), 1);
664 assert!(f.args[0].default.is_some(), "should have a default");
665 }
666
667 #[test]
668 fn security_definer() {
669 let f = build_fn(
670 "CREATE FUNCTION app.f() RETURNS void LANGUAGE sql SECURITY DEFINER \
671 AS $$ SELECT NULL $$;",
672 );
673 assert!(matches!(f.security, SecurityMode::Definer));
674 }
675}