From d173e82c3e490e26b4bb1bb1a4cf1c8b5f1c6fdb Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 9 Nov 2025 15:11:01 +0100 Subject: [PATCH 1/2] Enhance CREATE FUNCTION parsing to support C functions with multiple arguments and flexible attribute order --- src/ast/ddl.rs | 10 ++++++++- src/parser/mod.rs | 35 ++++++++++++++++++++--------- tests/sqlparser_postgres.rs | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index fd481213f..1860cd1d6 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2768,7 +2768,15 @@ impl fmt::Display for CreateFunction { write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; } if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { - write!(f, " AS {function_body}")?; + write!(f, " AS ")?; + // Special handling for tuple expressions to format without parentheses + // PostgreSQL C functions use: AS 'obj_file', 'link_symbol' + // rather than: AS ('obj_file', 'link_symbol') + if let Expr::Tuple(exprs) = function_body { + write!(f, "{}", display_comma_separated(exprs))?; + } else { + write!(f, "{function_body}")?; + } } if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9a01e510b..bd0ab0329 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10219,17 +10219,32 @@ 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 peek_token = self.peek_token(); - let span = peek_token.span; - match peek_token.token { - Token::DollarQuotedString(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => - { - self.next_token(); - Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) + // Helper closure to parse a single string value (quoted or dollar-quoted) + let parse_string_expr = |parser: &mut Parser| -> Result { + let peek_token = parser.peek_token(); + let span = peek_token.span; + match peek_token.token { + Token::DollarQuotedString(s) if dialect_of!(parser is PostgreSqlDialect | GenericDialect) => + { + parser.next_token(); + Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) + } + _ => Ok(Expr::Value( + Value::SingleQuotedString(parser.parse_literal_string()?).with_span(span), + )), } - _ => Ok(Expr::Value( - Value::SingleQuotedString(self.parse_literal_string()?).with_span(span), - )), + }; + + let first_expr = parse_string_expr(self)?; + + // Check if there's a comma, indicating multiple strings (e.g., AS 'obj_file', 'link_symbol') + // This is used for C language functions: AS 'MODULE_PATHNAME', 'link_symbol' + if self.consume_token(&Token::Comma) { + let mut exprs = vec![first_expr]; + exprs.extend(self.parse_comma_separated(parse_string_expr)?); + Ok(Expr::Tuple(exprs)) + } else { + Ok(first_expr) } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9ba0fb978..4cdb7355f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4482,6 +4482,50 @@ fn parse_incorrect_create_function_parallel() { assert!(pg().parse_sql_statements(sql).is_err()); } +#[test] +fn parse_create_function_c_with_module_pathname() { + let sql = "CREATE FUNCTION cas_in(input cstring) RETURNS cas LANGUAGE c IMMUTABLE PARALLEL SAFE AS 'MODULE_PATHNAME', 'cas_in_wrapper'"; + assert_eq!( + pg_and_generic().verified_stmt(sql), + Statement::CreateFunction(CreateFunction { + or_alter: false, + or_replace: false, + temporary: false, + name: ObjectName::from(vec![Ident::new("cas_in")]), + args: Some(vec![OperateFunctionArg::with_name( + "input", + DataType::Custom(ObjectName::from(vec![Ident::new("cstring")]), vec![]), + ),]), + return_type: Some(DataType::Custom( + ObjectName::from(vec![Ident::new("cas")]), + vec![] + )), + language: Some("c".into()), + behavior: Some(FunctionBehavior::Immutable), + called_on_null: None, + parallel: Some(FunctionParallel::Safe), + function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Tuple(vec![ + Expr::Value( + (Value::SingleQuotedString("MODULE_PATHNAME".into())).with_empty_span() + ), + Expr::Value((Value::SingleQuotedString("cas_in_wrapper".into())).with_empty_span()), + ]))), + if_not_exists: false, + using: None, + determinism_specifier: None, + options: None, + remote_connection: None, + }) + ); + + // Test that attribute order flexibility works (IMMUTABLE before LANGUAGE) + let sql_alt_order = "CREATE FUNCTION cas_in(input cstring) RETURNS cas IMMUTABLE PARALLEL SAFE LANGUAGE c AS 'MODULE_PATHNAME', 'cas_in_wrapper'"; + pg_and_generic().one_statement_parses_to( + sql_alt_order, + "CREATE FUNCTION cas_in(input cstring) RETURNS cas LANGUAGE c IMMUTABLE PARALLEL SAFE AS 'MODULE_PATHNAME', 'cas_in_wrapper'" + ); +} + #[test] fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; From b2acc60fcf5b23954ad5ff79b669bf98d096f8d5 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 9 Nov 2025 15:19:31 +0100 Subject: [PATCH 2/2] Resolved code smell --- src/ast/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 176d36545..95a98a18d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2787,10 +2787,11 @@ impl fmt::Display for Declare { } /// Sql options of a `CREATE TABLE` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableOptions { + #[default] None, /// Options specified using the `WITH` keyword. /// e.g. `WITH (description = "123")` @@ -2819,12 +2820,6 @@ pub enum CreateTableOptions { TableProperties(Vec), } -impl Default for CreateTableOptions { - fn default() -> Self { - Self::None - } -} - impl fmt::Display for CreateTableOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self {