Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")?;
Expand Down
13 changes: 12 additions & 1 deletion src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9099,8 +9099,19 @@ pub enum CreateFunctionBody {
/// OPTIONS(description="desc");
/// ```
///
/// For PostgreSQL C functions with object file and link symbol:
/// ```sql
/// CREATE FUNCTION cas_in(input cstring) RETURNS cas
/// AS 'MODULE_PATHNAME', 'cas_in_wrapper'
/// ```
///
/// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11
AsBeforeOptions(Expr),
AsBeforeOptions {
/// The primary expression (object file path for C functions, or regular expression)
body: Expr,
/// Optional link symbol for C language functions
link_symbol: Option<Expr>,
},
/// A function body expression using the 'AS' keyword and shows up
/// after any `OPTIONS` clause.
///
Expand Down
51 changes: 33 additions & 18 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?);
Expand Down Expand Up @@ -5298,15 +5296,15 @@ 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 {
or_alter: false,
or_replace,
temporary,
name,
function_body: Some(CreateFunctionBody::AsBeforeOptions(as_)),
function_body: Some(body),
using,
if_not_exists: false,
args: None,
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -10521,19 +10522,33 @@ 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> {
// Helper closure to parse a single string value (quoted or dollar-quoted)
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),
)),
}
};

// 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'
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
Expand Down
11 changes: 7 additions & 4 deletions tests/sqlparser_hive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
115 changes: 90 additions & 25 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4259,9 +4259,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,
Expand Down Expand Up @@ -4297,9 +4300,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,
Expand Down Expand Up @@ -4339,9 +4345,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,
Expand Down Expand Up @@ -4381,9 +4390,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,
Expand Down Expand Up @@ -4416,13 +4428,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,
Expand Down Expand Up @@ -4454,9 +4469,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,
Expand Down Expand Up @@ -4487,6 +4505,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";
Expand Down Expand Up @@ -6074,8 +6138,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(),
Expand All @@ -6085,7 +6149,8 @@ fn parse_trigger_related_functions() {
},
)
).with_empty_span()),
),
link_symbol: None,
},
),
behavior: None,
called_on_null: None,
Expand Down