Support PostgreSQL C Functions with Multiple AS Parameters (#2095)
Some checks failed
license / Release Audit Tool (RAT) (push) Has been cancelled
Rust / codestyle (push) Has been cancelled
Rust / lint (push) Has been cancelled
Rust / benchmark-lint (push) Has been cancelled
Rust / compile (push) Has been cancelled
Rust / docs (push) Has been cancelled
Rust / test (stable) (push) Has been cancelled
Rust / compile-no-std (push) Has been cancelled
Rust / test (beta) (push) Has been cancelled
Rust / test (nightly) (push) Has been cancelled

Co-authored-by: Ifeanyi Ubah <ify1992@yahoo.com>
This commit is contained in:
Luca Cappelletti 2025-11-26 11:21:57 +01:00 committed by GitHub
parent e380494eb0
commit 4beea9a4bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 50 deletions

View file

@ -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}")?;

View file

@ -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.
///

View file

@ -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

View file

@ -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,

View file

@ -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,