From 04e268b66ae53851dd0dbe39eee3c78dfe6af12d Mon Sep 17 00:00:00 2001 From: Alexandre Monteiro <175942190+MrBisonte@users.noreply.github.com> Date: Sat, 7 Nov 2026 19:07:22 +0100 Subject: [PATCH] PostgreSQL: Support CREATE OR REPLACE PROCEDURE --- .../postgres_create_or_replace_procedure.sql | 1 + src/ast/mod.rs | 6 ++- src/parser/mod.rs | 29 +++++++---- tests/sqlparser_common.rs | 48 +++++++++++++++++++ tests/sqlparser_mssql.rs | 1 + 5 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_create_or_replace_procedure.sql diff --git a/fuzz/fuzz_seeds/postgres_create_or_replace_procedure.sql b/fuzz/fuzz_seeds/postgres_create_or_replace_procedure.sql new file mode 100644 index 0000000000..e2af36302b --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_create_or_replace_procedure.sql @@ -0,0 +1 @@ +CREATE OR REPLACE PROCEDURE p (a INT) LANGUAGE sql BEGIN ATOMIC INSERT INTO t VALUES (a); END diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 473b11a37b..b87abb66ce 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4591,6 +4591,8 @@ pub enum Statement { CreateProcedure { /// `OR ALTER` flag. or_alter: bool, + /// `OR REPLACE` flag. + or_replace: bool, /// Procedure name. name: ObjectName, /// Parenthesized parameter list, `None` when absent. @@ -5612,14 +5614,16 @@ impl fmt::Display for Statement { Statement::CreateProcedure { name, or_alter, + or_replace, params, language, body, } => { write!( f, - "CREATE {or_alter}PROCEDURE {name}", + "CREATE {or_alter}{or_replace}PROCEDURE {name}", or_alter = if *or_alter { "OR ALTER " } else { "" }, + or_replace = if *or_replace { "OR REPLACE " } else { "" }, name = name )?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1d6cc53417..7081012287 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5527,9 +5527,7 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; - // `or_replace` is caught by an earlier arm. It stays so that reordering cannot bypass it. - let has_modifier = or_replace - || or_alter + let has_other_modifier = or_alter || temporary || global.is_some() || transient @@ -5537,6 +5535,8 @@ impl<'a> Parser<'a> { || multiset.is_some() || persistent || create_view_params.is_some(); + // `or_replace` is caught by an earlier arm. It stays so that reordering cannot bypass it. + let has_modifier = or_replace || has_other_modifier; if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { self.parse_create_snapshot_table().map(Into::into) } else if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { @@ -5599,9 +5599,19 @@ impl<'a> Parser<'a> { self.parse_create_schema(or_replace) } else if self.parse_keyword(Keyword::WAREHOUSE) { self.parse_create_warehouse(or_replace).map(Into::into) + } else if self.parse_keyword(Keyword::PROCEDURE) + || self.dialect.supports_create_proc_syntax() && self.parse_keyword(Keyword::PROC) + { + if or_replace && has_other_modifier { + return parser_err!( + "CREATE OR REPLACE PROCEDURE does not accept this modifier", + modifier_loc + ); + } + self.parse_create_procedure(or_alter, or_replace) } else if or_replace { self.expected_ref( - "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or AGGREGATE or SCHEMA or WAREHOUSE after CREATE OR REPLACE", + "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or PROCEDURE or AGGREGATE or SCHEMA or WAREHOUSE after CREATE OR REPLACE", self.peek_token_ref(), ) } else if self.parse_keyword(Keyword::EXTENSION) { @@ -5624,10 +5634,6 @@ impl<'a> Parser<'a> { self.parse_create_collation().map(Into::into) } else if self.parse_keyword(Keyword::TYPE) { self.parse_create_type() - } else if self.parse_keyword(Keyword::PROCEDURE) - || self.dialect.supports_create_proc_syntax() && self.parse_keyword(Keyword::PROC) - { - self.parse_create_procedure(or_alter) } else if self.parse_keyword(Keyword::CONNECTOR) { self.parse_create_connector().map(Into::into) } else if self.parse_keyword(Keyword::OPERATOR) { @@ -21971,7 +21977,11 @@ impl<'a> Parser<'a> { } /// Parse `CREATE PROCEDURE` statement. - pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result { + pub fn parse_create_procedure( + &mut self, + or_alter: bool, + or_replace: bool, + ) -> Result { let name = self.parse_object_name(false)?; let params = self.parse_optional_procedure_parameters()?; @@ -21992,6 +22002,7 @@ impl<'a> Parser<'a> { Ok(Statement::CreateProcedure { name, or_alter, + or_replace, params, language, body, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aeb3e67a47..7d37436124 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23481,3 +23481,51 @@ fn parse_trailing_end_keyword_is_not_silently_dropped() { ParserError::ParserError("Expected: end of statement, found: END".to_string()) ); } + +#[test] +fn parse_create_or_replace_procedure() { + match verified_stmt("CREATE OR REPLACE PROCEDURE p () LANGUAGE sql BEGIN ATOMIC SELECT 1; END") + { + Statement::CreateProcedure { + or_replace, + or_alter, + .. + } => { + assert!(or_replace); + assert!(!or_alter); + } + stmt => panic!("unexpected statement: {stmt:?}"), + } + + match verified_stmt("CREATE PROCEDURE p () LANGUAGE sql BEGIN ATOMIC SELECT 1; END") { + Statement::CreateProcedure { or_replace, .. } => assert!(!or_replace), + stmt => panic!("unexpected statement: {stmt:?}"), + } + + verified_stmt("CREATE OR REPLACE PROCEDURE p (a INT) AS BEGIN SELECT 1; END"); + + for (sql, err) in [ + ( + "CREATE OR REPLACE PROCEDURE", + "Expected: identifier, found: EOF", + ), + ( + "CREATE OR REPLACE OR ALTER PROCEDURE p AS BEGIN SELECT 1; END", + "CREATE OR REPLACE PROCEDURE does not accept this modifier", + ), + ( + "CREATE OR REPLACE TEMPORARY PROCEDURE p AS BEGIN SELECT 1; END", + "CREATE OR REPLACE PROCEDURE does not accept this modifier", + ), + ( + "CREATE OR REPLACE EXTENSION e", + "Expected: [EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or PROCEDURE or AGGREGATE or SCHEMA or WAREHOUSE after CREATE OR REPLACE, found: EXTENSION", + ), + ] { + assert_eq!( + parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(err.to_string()), + "{sql}" + ); + } +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index cadcabf035..f5f9b135fe 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -134,6 +134,7 @@ fn parse_create_procedure() { ms().verified_stmt(sql), Statement::CreateProcedure { or_alter: true, + or_replace: false, body: CreateProcedureBody::As(ConditionalStatements::BeginEnd(BeginEndStatements { begin_token: AttachedToken::empty(), statements: vec![Statement::Query(Box::new(Query {