This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 4beea9a4 Support PostgreSQL C Functions with Multiple AS Parameters
(#2095)
4beea9a4 is described below
commit 4beea9a4bccb867b34f466f0ad1793ebcdf370ba
Author: Luca Cappelletti <[email protected]>
AuthorDate: Wed Nov 26 11:21:57 2025 +0100
Support PostgreSQL C Functions with Multiple AS Parameters (#2095)
Co-authored-by: Ifeanyi Ubah <[email protected]>
---
src/ast/ddl.rs | 8 ++-
src/ast/mod.rs | 15 +++++-
src/parser/mod.rs | 48 +++++++++++-------
tests/sqlparser_hive.rs | 11 +++--
tests/sqlparser_postgres.rs | 115 ++++++++++++++++++++++++++++++++++----------
5 files changed, 147 insertions(+), 50 deletions(-)
diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 6f5010f0..078e3623 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 8b50452c..2d768c24 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<Expr>,
+ },
/// 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 759f5189..0d561089 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<Expr,
ParserError> {
- 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<CreateFunctionBody, ParserError> {
+ let parse_string_expr = |parser: &mut Parser| -> Result<Expr,
ParserError> {
+ 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 56a72ec8..386bab7f 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 7309a6ba..91150666 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 EX [...]
@@ -6081,7 +6145,8 @@ fn parse_trigger_related_functions() {
},
)
).with_empty_span()),
- ),
+ link_symbol: None,
+ },
),
behavior: None,
called_on_null: None,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]