-
Notifications
You must be signed in to change notification settings - Fork 606
Add support for table valued functions for SQL Server #1839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e2c8b8b
0288177
25a4416
b84ec50
c2fd171
9e45f27
10919b5
6798bcc
846e5c9
e36933c
0e11006
4dd05d1
486f85a
8bc8d38
9b6d6b1
0add6e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8778,6 +8778,30 @@ pub enum CreateFunctionBody { | |
/// | ||
/// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html | ||
Return(Expr), | ||
|
||
/// Function body expression using the 'AS RETURN' keywords | ||
/// | ||
/// Example: | ||
/// ```sql | ||
/// CREATE FUNCTION myfunc(a INT, b INT) | ||
/// RETURNS TABLE | ||
/// AS RETURN (SELECT a + b AS sum); | ||
/// ``` | ||
/// | ||
/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql | ||
AsReturnExpr(Expr), | ||
|
||
/// Function body expression using the 'AS RETURN' keywords, with an un-parenthesized SELECT query | ||
/// | ||
/// Example: | ||
/// ```sql | ||
/// CREATE FUNCTION myfunc(a INT, b INT) | ||
/// RETURNS TABLE | ||
/// AS RETURN SELECT a + b AS sum; | ||
/// ``` | ||
/// | ||
/// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#select_stmt | ||
AsReturnSelect(Select), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we can also include a reference doc link here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5204,19 +5204,62 @@ impl<'a> Parser<'a> { | |
let (name, args) = self.parse_create_function_name_and_params()?; | ||
|
||
self.expect_keyword(Keyword::RETURNS)?; | ||
let return_type = Some(self.parse_data_type()?); | ||
|
||
self.expect_keyword_is(Keyword::AS)?; | ||
let return_table = self.maybe_parse(|p| { | ||
let return_table_name = p.parse_identifier()?; | ||
|
||
p.expect_keyword_is(Keyword::TABLE)?; | ||
p.prev_token(); | ||
|
||
let table_column_defs = match p.parse_data_type()? { | ||
DataType::Table(Some(table_column_defs)) if !table_column_defs.is_empty() => { | ||
table_column_defs | ||
} | ||
_ => parser_err!( | ||
"Expected table column definitions after TABLE keyword", | ||
p.peek_token().span.start | ||
)?, | ||
}; | ||
|
||
Ok(DataType::NamedTable { | ||
name: ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), | ||
columns: table_column_defs, | ||
}) | ||
})?; | ||
|
||
let begin_token = self.expect_keyword(Keyword::BEGIN)?; | ||
let statements = self.parse_statement_list(&[Keyword::END])?; | ||
let end_token = self.expect_keyword(Keyword::END)?; | ||
let return_type = if return_table.is_some() { | ||
return_table | ||
} else { | ||
Some(self.parse_data_type()?) | ||
}; | ||
iffyio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let function_body = Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { | ||
begin_token: AttachedToken(begin_token), | ||
statements, | ||
end_token: AttachedToken(end_token), | ||
})); | ||
let _ = self.parse_keyword(Keyword::AS); | ||
|
||
let function_body = if self.peek_keyword(Keyword::BEGIN) { | ||
let begin_token = self.expect_keyword(Keyword::BEGIN)?; | ||
let statements = self.parse_statement_list(&[Keyword::END])?; | ||
let end_token = self.expect_keyword(Keyword::END)?; | ||
|
||
Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { | ||
begin_token: AttachedToken(begin_token), | ||
statements, | ||
end_token: AttachedToken(end_token), | ||
})) | ||
} else if self.parse_keyword(Keyword::RETURN) { | ||
if self.peek_token() == Token::LParen { | ||
Some(CreateFunctionBody::AsReturnExpr(self.parse_expr()?)) | ||
} else if self.peek_keyword(Keyword::SELECT) { | ||
let select = self.parse_select()?; | ||
Some(CreateFunctionBody::AsReturnSelect(select)) | ||
} else { | ||
parser_err!( | ||
"Expected a subquery (or bare SELECT statement) after RETURN", | ||
self.peek_token().span.start | ||
)? | ||
} | ||
Comment on lines
+5255
to
+5259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a test for this behavior? (e.g. one that passes a regular expression and the verifies that the parser rejects it) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
} else { | ||
parser_err!("Unparsable function body", self.peek_token().span.start)? | ||
}; | ||
|
||
Ok(Statement::CreateFunction(CreateFunction { | ||
or_alter, | ||
|
@@ -9784,8 +9827,14 @@ impl<'a> Parser<'a> { | |
Ok(DataType::AnyType) | ||
} | ||
Keyword::TABLE => { | ||
let columns = self.parse_returns_table_columns()?; | ||
Ok(DataType::Table(columns)) | ||
// an LParen after the TABLE keyword indicates that table columns are being defined | ||
// whereas no LParen indicates an anonymous table expression will be returned | ||
if self.peek_token() == Token::LParen { | ||
let columns = self.parse_returns_table_columns()?; | ||
Ok(DataType::Table(Some(columns))) | ||
} else { | ||
Ok(DataType::Table(None)) | ||
} | ||
} | ||
Keyword::SIGNED => { | ||
if self.parse_keyword(Keyword::INTEGER) { | ||
|
@@ -9826,13 +9875,7 @@ impl<'a> Parser<'a> { | |
} | ||
|
||
fn parse_returns_table_column(&mut self) -> Result<ColumnDef, ParserError> { | ||
let name = self.parse_identifier()?; | ||
let data_type = self.parse_data_type()?; | ||
Ok(ColumnDef { | ||
name, | ||
data_type, | ||
options: Vec::new(), // No constraints expected here | ||
}) | ||
self.parse_column_def() | ||
} | ||
|
||
fn parse_returns_table_columns(&mut self) -> Result<Vec<ColumnDef>, ParserError> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -254,6 +254,12 @@ fn parse_create_function() { | |
"; | ||
let _ = ms().verified_stmt(multi_statement_function); | ||
|
||
let multi_statement_function_without_as = multi_statement_function.replace(" AS", ""); | ||
let _ = ms().one_statement_parses_to( | ||
&multi_statement_function_without_as, | ||
multi_statement_function, | ||
); | ||
|
||
let create_function_with_conditional = "\ | ||
CREATE FUNCTION some_scalar_udf() \ | ||
RETURNS INT \ | ||
|
@@ -288,6 +294,87 @@ fn parse_create_function() { | |
END\ | ||
"; | ||
let _ = ms().verified_stmt(create_function_with_return_expression); | ||
|
||
let create_inline_table_value_function = "\ | ||
CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS TABLE \ | ||
AS \ | ||
RETURN (SELECT 1 AS col_1)\ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parentheses are optional for inline tvf There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added support for that syntax & added a new test case example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"; | ||
let _ = ms().verified_stmt(create_inline_table_value_function); | ||
|
||
let create_inline_table_value_function_without_parentheses = "\ | ||
CREATE FUNCTION some_inline_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS TABLE \ | ||
AS \ | ||
RETURN SELECT 1 AS col_1\ | ||
"; | ||
let _ = ms().verified_stmt(create_inline_table_value_function_without_parentheses); | ||
|
||
let create_inline_table_value_function_without_as = | ||
create_inline_table_value_function.replace(" AS", ""); | ||
let _ = ms().one_statement_parses_to( | ||
&create_inline_table_value_function_without_as, | ||
create_inline_table_value_function, | ||
); | ||
|
||
let create_multi_statement_table_value_function = "\ | ||
CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS @t TABLE (col_1 INT) \ | ||
AS \ | ||
BEGIN \ | ||
INSERT INTO @t SELECT 1; \ | ||
RETURN; \ | ||
END\ | ||
"; | ||
let _ = ms().verified_stmt(create_multi_statement_table_value_function); | ||
|
||
let create_multi_statement_table_value_function_without_as = | ||
create_multi_statement_table_value_function.replace(" AS", ""); | ||
let _ = ms().one_statement_parses_to( | ||
&create_multi_statement_table_value_function_without_as, | ||
create_multi_statement_table_value_function, | ||
); | ||
|
||
let create_multi_statement_table_value_function_with_constraints = "\ | ||
CREATE FUNCTION some_multi_statement_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS @t TABLE (col_1 INT NOT NULL) \ | ||
AS \ | ||
BEGIN \ | ||
INSERT INTO @t SELECT 1; \ | ||
RETURN @t; \ | ||
END\ | ||
"; | ||
let _ = ms().verified_stmt(create_multi_statement_table_value_function_with_constraints); | ||
|
||
let create_multi_statement_tvf_without_table_definition = "\ | ||
CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS @t TABLE () | ||
AS \ | ||
BEGIN \ | ||
INSERT INTO @t SELECT 1; \ | ||
RETURN @t; \ | ||
END\ | ||
"; | ||
assert_eq!( | ||
ParserError::ParserError("Unparsable function body".to_owned()), | ||
ms().parse_sql_statements(create_multi_statement_tvf_without_table_definition) | ||
.unwrap_err() | ||
); | ||
|
||
let create_inline_tvf_without_subquery_or_bare_select = "\ | ||
CREATE FUNCTION incorrect_tvf(@foo INT, @bar VARCHAR(256)) \ | ||
RETURNS TABLE | ||
AS \ | ||
RETURN 'hi'\ | ||
"; | ||
assert_eq!( | ||
ParserError::ParserError( | ||
"Expected a subquery (or bare SELECT statement) after RETURN".to_owned() | ||
), | ||
ms().parse_sql_statements(create_inline_tvf_without_subquery_or_bare_select) | ||
.unwrap_err() | ||
); | ||
} | ||
|
||
#[test] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add a link to the docs that support
NamedTable
variant?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍