From 484fac1ca8f47fae57fa095c24c64f9970c7b216 Mon Sep 17 00:00:00 2001 From: Alexandre Monteiro <175942190+MrBisonte@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:30:01 +0100 Subject: [PATCH 1/2] PostgreSQL: Support string bodies in CREATE PROCEDURE --- .../postgres_create_procedure_string_body.sql | 1 + src/ast/mod.rs | 16 +++++ src/parser/mod.rs | 37 ++++++++---- tests/sqlparser_common.rs | 59 +++++++++++++++++++ 4 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_create_procedure_string_body.sql diff --git a/fuzz/fuzz_seeds/postgres_create_procedure_string_body.sql b/fuzz/fuzz_seeds/postgres_create_procedure_string_body.sql new file mode 100644 index 0000000000..6eb5756e10 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_create_procedure_string_body.sql @@ -0,0 +1 @@ +CREATE PROCEDURE p (a INT) LANGUAGE plpgsql AS $$ BEGIN INSERT INTO t VALUES (a); END; $$ diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 473b11a37b..1f4ddd4de8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5633,6 +5633,13 @@ impl fmt::Display for Statement { match body { CreateProcedureBody::As(body) => write!(f, " AS {body}"), + CreateProcedureBody::AsString { body, link_symbol } => { + write!(f, " AS {body}")?; + if let Some(link_symbol) = link_symbol { + write!(f, ", {link_symbol}")?; + } + Ok(()) + } CreateProcedureBody::BeginAtomic(BeginEndStatements { statements, .. }) => { format_begin_atomic(f, statements) } @@ -10611,6 +10618,15 @@ pub enum CreateFunctionBody { pub enum CreateProcedureBody { /// Statements after `AS`. As(ConditionalStatements), + /// A definition string after `AS`, e.g. `AS $$ body $$` or `AS 'obj_file', 'link_symbol'`. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createprocedure.html + AsString { + /// The definition. + body: Expr, + /// The symbol to link when the definition names a shared library file. + link_symbol: Option, + }, /// SQL-standard body enclosed by `BEGIN ATOMIC` and `END`. /// /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createprocedure.html diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 76bd114071..f86e9b3893 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13515,6 +13515,13 @@ impl<'a> Parser<'a> { /// Parse the body of a `CREATE FUNCTION` specified as a string. /// e.g. `CREATE FUNCTION ... AS $$ body $$`. fn parse_create_function_body_string(&mut self) -> Result { + let (body, link_symbol) = self.parse_definition_string()?; + Ok(CreateFunctionBody::AsBeforeOptions { body, link_symbol }) + } + + /// Parse a definition string and the link symbol that may follow it, + /// e.g. `$$ body $$` or `'obj_file', 'link_symbol'`. + fn parse_definition_string(&mut self) -> Result<(Expr, Option), ParserError> { let parse_string_expr = |parser: &mut Parser| -> Result { let peek_token = parser.peek_token(); let span = peek_token.span; @@ -13530,14 +13537,13 @@ impl<'a> Parser<'a> { } }; - Ok(CreateFunctionBody::AsBeforeOptions { - body: parse_string_expr(self)?, - link_symbol: if self.consume_token(&Token::Comma) { - Some(parse_string_expr(self)?) - } else { - None - }, - }) + let body = parse_string_expr(self)?; + let link_symbol = if self.consume_token(&Token::Comma) { + Some(parse_string_expr(self)?) + } else { + None + }; + Ok((body, link_symbol)) } /// Parse a literal string @@ -22010,7 +22016,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; - let language = if self.parse_keyword(Keyword::LANGUAGE) { + let mut language = if self.parse_keyword(Keyword::LANGUAGE) { Some(self.parse_identifier()?) } else { None @@ -22020,7 +22026,18 @@ impl<'a> Parser<'a> { Some(statements) => CreateProcedureBody::BeginAtomic(statements), None => { self.expect_keyword_is(Keyword::AS)?; - CreateProcedureBody::As(self.parse_conditional_statements(&[Keyword::END])?) + if matches!( + self.peek_token_ref().token, + Token::SingleQuotedString(_) | Token::DollarQuotedString(_) + ) { + let (body, link_symbol) = self.parse_definition_string()?; + if language.is_none() && self.parse_keyword(Keyword::LANGUAGE) { + language = Some(self.parse_identifier()?); + } + CreateProcedureBody::AsString { body, link_symbol } + } else { + CreateProcedureBody::As(self.parse_conditional_statements(&[Keyword::END])?) + } } }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cf4f11c994..51eb4f69b7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23516,3 +23516,62 @@ fn parse_national_character_string_types() { ParserError::ParserError("Expected: ), found: EOF".to_string()) ); } + +#[test] +fn parse_create_procedure_string_body() { + match verified_stmt("CREATE PROCEDURE p (a INT) LANGUAGE sql AS 'INSERT INTO t VALUES (a)'") { + Statement::CreateProcedure { + language, + body: CreateProcedureBody::AsString { body, link_symbol }, + .. + } => { + assert_eq!(language, Some(Ident::new("sql"))); + assert_eq!( + body, + Expr::Value( + Value::SingleQuotedString("INSERT INTO t VALUES (a)".to_string()) + .with_empty_span() + ) + ); + assert_eq!(link_symbol, None); + } + stmt => panic!("unexpected statement: {stmt:?}"), + } + + verified_stmt("CREATE PROCEDURE p () LANGUAGE C AS 'MODULE_PATHNAME', 'p_wrapper'"); + one_statement_parses_to( + "CREATE PROCEDURE p () AS 'SELECT 1' LANGUAGE sql", + "CREATE PROCEDURE p () LANGUAGE sql AS 'SELECT 1'", + ); + + pg_and_generic().verified_stmt( + "CREATE PROCEDURE p (a INT) LANGUAGE plpgsql AS $$ BEGIN INSERT INTO t VALUES (a); END; $$", + ); + pg_and_generic().verified_stmt("CREATE PROCEDURE p () LANGUAGE sql AS $body$ SELECT 1 $body$"); + pg_and_generic().one_statement_parses_to( + r#"CREATE PROCEDURE insert_data(a integer, b integer) +LANGUAGE SQL +AS $$ +INSERT INTO tbl VALUES (a); +INSERT INTO tbl VALUES (b); +$$"#, + "CREATE PROCEDURE insert_data (a INTEGER, b INTEGER) LANGUAGE SQL AS $$\nINSERT INTO tbl VALUES (a);\nINSERT INTO tbl VALUES (b);\n$$", + ); + + for (sql, err) in [ + ( + "CREATE PROCEDURE p () LANGUAGE sql AS 'SELECT 1' LANGUAGE sql", + "Expected: end of statement, found: LANGUAGE", + ), + ( + "CREATE PROCEDURE p () LANGUAGE C AS 'MODULE_PATHNAME',", + "Expected: literal string, found: EOF", + ), + ] { + assert_eq!( + parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(err.to_string()), + "{sql}" + ); + } +} From 477cf3b9e4153c9250242e58b66ff512f965ef9b Mon Sep 17 00:00:00 2001 From: Alexandre Monteiro <175942190+MrBisonte@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:46:11 +0100 Subject: [PATCH 2/2] Accept escape and Unicode string bodies in CREATE PROCEDURE Co-authored-by: Luca Cappelletti --- src/parser/mod.rs | 5 ++++- tests/sqlparser_common.rs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f86e9b3893..aba553b839 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -22028,7 +22028,10 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::AS)?; if matches!( self.peek_token_ref().token, - Token::SingleQuotedString(_) | Token::DollarQuotedString(_) + Token::SingleQuotedString(_) + | Token::DollarQuotedString(_) + | Token::EscapedStringLiteral(_) + | Token::UnicodeStringLiteral(_) ) { let (body, link_symbol) = self.parse_definition_string()?; if language.is_none() && self.parse_keyword(Keyword::LANGUAGE) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 51eb4f69b7..96e1649c68 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23548,6 +23548,10 @@ fn parse_create_procedure_string_body() { "CREATE PROCEDURE p (a INT) LANGUAGE plpgsql AS $$ BEGIN INSERT INTO t VALUES (a); END; $$", ); pg_and_generic().verified_stmt("CREATE PROCEDURE p () LANGUAGE sql AS $body$ SELECT 1 $body$"); + pg_and_generic().one_statement_parses_to( + "CREATE PROCEDURE p () LANGUAGE sql AS E'SELECT 1'", + "CREATE PROCEDURE p () LANGUAGE sql AS 'SELECT 1'", + ); pg_and_generic().one_statement_parses_to( r#"CREATE PROCEDURE insert_data(a integer, b integer) LANGUAGE SQL