diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6f5010f04..078e3623e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3099,8 +3099,12 @@ impl fmt::Display for CreateFunction { if let Some(remote_connection) = &self.remote_connection { write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; } - if let Some(CreateFunctionBody::AsBeforeOptions(function_body)) = &self.function_body { - write!(f, " AS {function_body}")?; + if let Some(CreateFunctionBody::AsBeforeOptions { body, link_symbol }) = &self.function_body + { + write!(f, " AS {body}")?; + if let Some(link_symbol) = link_symbol { + write!(f, ", {link_symbol}")?; + } } if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8b50452cc..2d768c246 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9108,7 +9108,20 @@ pub enum CreateFunctionBody { /// ``` /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 - AsBeforeOptions(Expr), + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + AsBeforeOptions { + /// The primary expression. + body: Expr, + /// Link symbol if the primary expression contains the name of shared library file. + /// + /// Example: + /// ```sql + /// CREATE FUNCTION cas_in(input cstring) RETURNS cas + /// AS 'MODULE_PATHNAME', 'cas_in_wrapper' + /// ``` + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + link_symbol: Option, + }, /// A function body expression using the 'AS' keyword and shows up /// after any `OPTIONS` clause. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 759f51892..0d5610894 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5204,9 +5204,7 @@ impl<'a> Parser<'a> { } if self.parse_keyword(Keyword::AS) { ensure_not_set(&body.function_body, "AS")?; - body.function_body = Some(CreateFunctionBody::AsBeforeOptions( - self.parse_create_function_body_string()?, - )); + body.function_body = Some(self.parse_create_function_body_string()?); } else if self.parse_keyword(Keyword::LANGUAGE) { ensure_not_set(&body.language, "LANGUAGE")?; body.language = Some(self.parse_identifier()?); @@ -5298,7 +5296,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(false)?; self.expect_keyword_is(Keyword::AS)?; - let as_ = self.parse_create_function_body_string()?; + let body = self.parse_create_function_body_string()?; let using = self.parse_optional_create_function_using()?; Ok(Statement::CreateFunction(CreateFunction { @@ -5306,7 +5304,7 @@ impl<'a> Parser<'a> { or_replace, temporary, name, - function_body: Some(CreateFunctionBody::AsBeforeOptions(as_)), + function_body: Some(body), using, if_not_exists: false, args: None, @@ -5368,7 +5366,10 @@ impl<'a> Parser<'a> { let expr = self.parse_expr()?; if options.is_none() { options = self.maybe_parse_options(Keyword::OPTIONS)?; - Some(CreateFunctionBody::AsBeforeOptions(expr)) + Some(CreateFunctionBody::AsBeforeOptions { + body: expr, + link_symbol: None, + }) } else { Some(CreateFunctionBody::AsAfterOptions(expr)) } @@ -10574,19 +10575,30 @@ 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))) + fn parse_create_function_body_string(&mut self) -> Result { + 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), - )), - } + }; + + Ok(CreateFunctionBody::AsBeforeOptions { + body: parse_string_expr(self)?, + link_symbol: if self.consume_token(&Token::Comma) { + Some(parse_string_expr(self)?) + } else { + None + }, + }) } /// Parse a literal string diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 56a72ec84..386bab7f0 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -408,10 +408,13 @@ fn parse_create_function() { assert_eq!(name.to_string(), "mydb.myfunc"); assert_eq!( function_body, - Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::SingleQuotedString("org.random.class.Name".to_string())) - .with_empty_span() - ))) + Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::SingleQuotedString("org.random.class.Name".to_string())) + .with_empty_span() + ), + link_symbol: None, + }) ); assert_eq!( using, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7309a6baa..911506668 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4260,9 +4260,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4298,9 +4301,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4340,9 +4346,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4382,9 +4391,12 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4417,13 +4429,16 @@ $$"#; behavior: None, called_on_null: None, parallel: None, - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::DollarQuotedString(DollarQuotedString { - value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), - tag: None - })) - .with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::DollarQuotedString(DollarQuotedString { + value: "\n BEGIN\n RETURN TRUE;\n END;\n ".to_owned(), + tag: None + })) + .with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4455,9 +4470,12 @@ fn parse_create_function() { behavior: Some(FunctionBehavior::Immutable), called_on_null: Some(FunctionCalledOnNull::Strict), parallel: Some(FunctionParallel::Safe), - function_body: Some(CreateFunctionBody::AsBeforeOptions(Expr::Value( - (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() - ))), + function_body: Some(CreateFunctionBody::AsBeforeOptions { + body: Expr::Value( + (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() + ), + link_symbol: None, + }), if_not_exists: false, using: None, determinism_specifier: None, @@ -4488,6 +4506,52 @@ 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 { + body: Expr::Value( + (Value::SingleQuotedString("MODULE_PATHNAME".into())).with_empty_span() + ), + link_symbol: Some(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"; @@ -6070,8 +6134,8 @@ fn parse_trigger_related_functions() { args: Some(vec![]), return_type: Some(DataType::Trigger), function_body: Some( - CreateFunctionBody::AsBeforeOptions( - Expr::Value(( + CreateFunctionBody::AsBeforeOptions { + body: Expr::Value(( Value::DollarQuotedString( DollarQuotedString { value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(), @@ -6081,7 +6145,8 @@ fn parse_trigger_related_functions() { }, ) ).with_empty_span()), - ), + link_symbol: None, + }, ), behavior: None, called_on_null: None,