From 54b282913b44c79ea785696e7debcd51ce933617 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 14:18:15 -0400 Subject: [PATCH 1/8] Add support for inline table valued functions for SQL Server --- src/ast/data_type.rs | 8 +++++++- src/ast/ddl.rs | 3 +++ src/ast/mod.rs | 12 ++++++++++++ src/parser/mod.rs | 35 +++++++++++++++++++++++++---------- tests/sqlparser_mssql.rs | 8 ++++++++ 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 52919de8a..7212ddc9f 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -48,6 +48,7 @@ pub enum DataType { /// Table type in [PostgreSQL], e.g. CREATE FUNCTION RETURNS TABLE(...). /// /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html + /// [MsSQL]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#c-create-a-multi-statement-table-valued-function Table(Vec), /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), @@ -716,7 +717,12 @@ impl fmt::Display for DataType { DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), - DataType::Table(fields) => write!(f, "TABLE({})", display_comma_separated(fields)), + DataType::Table(fields) => { + if fields.is_empty() { + return write!(f, "TABLE"); + } + write!(f, "TABLE({})", display_comma_separated(fields)) + } DataType::GeometricType(kind) => write!(f, "{}", kind), } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a457a0655..c583bb3c9 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2313,6 +2313,9 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; } + if let Some(CreateFunctionBody::AsReturn(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } if let Some(using) = &self.using { write!(f, " {using}")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6b7ba12d9..ca36b8420 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8660,6 +8660,18 @@ 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 + AsReturn(Expr), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d18c7f694..a95fe4cbb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5207,15 +5207,26 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::AS)?; - 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 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)?; - let function_body = Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { - begin_token: AttachedToken(begin_token), - statements, - end_token: AttachedToken(end_token), - })); + Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + })) + } else if self.peek_keyword(Keyword::RETURN) { + self.expect_keyword(Keyword::RETURN)?; + let expr = self.parse_expr()?; + if !matches!(expr, Expr::Subquery(_)) { + parser_err!("Expected a subquery after RETURN", expr.span().start)?; + } + Some(CreateFunctionBody::AsReturn(expr)) + } else { + parser_err!("Unparsable function body", self.peek_token().span.start)? + }; Ok(Statement::CreateFunction(CreateFunction { or_alter, @@ -9766,8 +9777,12 @@ impl<'a> Parser<'a> { Ok(DataType::AnyType) } Keyword::TABLE => { - let columns = self.parse_returns_table_columns()?; - Ok(DataType::Table(columns)) + if self.peek_keyword(Keyword::AS) { + Ok(DataType::Table(Vec::::new())) + } else { + let columns = self.parse_returns_table_columns()?; + Ok(DataType::Table(columns)) + } } Keyword::SIGNED => { if self.parse_keyword(Keyword::INTEGER) { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1c0a00b16..fc062fd9c 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -288,6 +288,14 @@ 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)\ + "; + let _ = ms().verified_stmt(create_inline_table_value_function); } #[test] From 4793aeb8d3c755bb8600bd2b0705ce664b3bf454 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 15:12:21 -0400 Subject: [PATCH 2/8] Add multi-statement table valued function support for SQL Server --- src/ast/data_type.rs | 10 ++++++++++ src/parser/mod.rs | 26 +++++++++++++++++++++++++- tests/sqlparser_mssql.rs | 11 +++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 7212ddc9f..0f1cafe7a 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -50,6 +50,13 @@ pub enum DataType { /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html /// [MsSQL]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#c-create-a-multi-statement-table-valued-function Table(Vec), + /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). + NamedTable( + /// Table name. + ObjectName, + /// Table columns. + Vec, + ), /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -723,6 +730,9 @@ impl fmt::Display for DataType { } write!(f, "TABLE({})", display_comma_separated(fields)) } + DataType::NamedTable(name, fields) => { + write!(f, "{} TABLE ({})", name, display_comma_separated(fields)) + } DataType::GeometricType(kind) => write!(f, "{}", kind), } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a95fe4cbb..dfb19534a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5203,7 +5203,31 @@ 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()?); + + let return_table = self.maybe_parse(|p| { + let return_table_name = p.parse_identifier()?; + let table_column_defs = if p.peek_keyword(Keyword::TABLE) { + match p.parse_data_type()? { + DataType::Table(t) => t, + _ => parser_err!( + "Expected table data type after TABLE keyword", + p.peek_token().span.start + )?, + } + } else { + parser_err!("Expected TABLE keyword after return type", p.peek_token().span.start)? + }; + Ok(DataType::NamedTable( + ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), + table_column_defs.clone(), + )) + })?; + + let return_type = if return_table.is_some() { + return_table + } else { + Some(self.parse_data_type()?) + }; self.expect_keyword_is(Keyword::AS)?; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index fc062fd9c..91af55c75 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -296,6 +296,17 @@ fn parse_create_function() { RETURN (SELECT 1 AS col_1)\ "; let _ = ms().verified_stmt(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); } #[test] From 9f08de01745aee84f35d690cc6a7c9e286b52875 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 16:07:05 -0400 Subject: [PATCH 3/8] Enable parsing `CREATE FUNCTION` without `AS` --- src/parser/mod.rs | 6 ++++-- tests/sqlparser_mssql.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dfb19534a..50270299c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5229,7 +5229,9 @@ impl<'a> Parser<'a> { Some(self.parse_data_type()?) }; - self.expect_keyword_is(Keyword::AS)?; + if self.peek_keyword(Keyword::AS) { + self.expect_keyword_is(Keyword::AS)?; + } let function_body = if self.peek_keyword(Keyword::BEGIN) { let begin_token = self.expect_keyword(Keyword::BEGIN)?; @@ -9801,7 +9803,7 @@ impl<'a> Parser<'a> { Ok(DataType::AnyType) } Keyword::TABLE => { - if self.peek_keyword(Keyword::AS) { + if self.peek_token() != Token::LParen { Ok(DataType::Table(Vec::::new())) } else { let columns = self.parse_returns_table_columns()?; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 91af55c75..c4571e1f4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -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 \ @@ -297,6 +303,13 @@ fn parse_create_function() { "; let _ = ms().verified_stmt(create_inline_table_value_function); + 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) \ @@ -307,6 +320,13 @@ fn parse_create_function() { 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, + ); } #[test] From a54aed6dc6d2ff686b1a389e99800e24d75c5175 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 16:39:37 -0400 Subject: [PATCH 4/8] Add support for constraints in table valued function definitions --- src/parser/mod.rs | 8 +------- tests/sqlparser_mssql.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 50270299c..1c98b2c7b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9849,13 +9849,7 @@ impl<'a> Parser<'a> { } fn parse_returns_table_column(&mut self) -> Result { - 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, ParserError> { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c4571e1f4..c0127c6fa 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -327,6 +327,17 @@ fn parse_create_function() { &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); } #[test] From 6346ecc840c7c6f37b3e39b665c198f4bcb664d3 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 19:38:52 -0400 Subject: [PATCH 5/8] Corrected syntax formatting --- src/parser/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1c98b2c7b..22236255d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5215,7 +5215,10 @@ impl<'a> Parser<'a> { )?, } } else { - parser_err!("Expected TABLE keyword after return type", p.peek_token().span.start)? + parser_err!( + "Expected TABLE keyword after return type", + p.peek_token().span.start + )? }; Ok(DataType::NamedTable( ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), From a27c22ce21753b47ea639bd260695c9368492481 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 7 May 2025 16:39:32 -0400 Subject: [PATCH 6/8] Add support for un-parenthesized "RETURN SELECT" syntax - rename `AsReturn` to `AsReturnSubquery` for clarity between these two variants --- src/ast/ddl.rs | 5 ++++- src/ast/mod.rs | 12 +++++++++++- src/parser/mod.rs | 22 ++++++++++++++++++---- tests/sqlparser_mssql.rs | 8 ++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c583bb3c9..14f909a87 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2313,7 +2313,10 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; } - if let Some(CreateFunctionBody::AsReturn(function_body)) = &self.function_body { + if let Some(CreateFunctionBody::AsReturnSubquery(function_body)) = &self.function_body { + write!(f, " AS RETURN {function_body}")?; + } + if let Some(CreateFunctionBody::AsReturnSelect(function_body)) = &self.function_body { write!(f, " AS RETURN {function_body}")?; } if let Some(using) = &self.using { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ca36b8420..b58075372 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8671,7 +8671,17 @@ pub enum CreateFunctionBody { /// ``` /// /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql - AsReturn(Expr), + AsReturnSubquery(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; + /// ``` + AsReturnSelect(Select), } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 22236255d..77454eed1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5248,11 +5248,25 @@ impl<'a> Parser<'a> { })) } else if self.peek_keyword(Keyword::RETURN) { self.expect_keyword(Keyword::RETURN)?; - let expr = self.parse_expr()?; - if !matches!(expr, Expr::Subquery(_)) { - parser_err!("Expected a subquery after RETURN", expr.span().start)?; + + if self.peek_token() == Token::LParen { + let expr = self.parse_expr()?; + if !matches!(expr, Expr::Subquery(_)) { + parser_err!( + "Expected a subquery after RETURN", + self.peek_token().span.start + )? + } + Some(CreateFunctionBody::AsReturnSubquery(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 + )? } - Some(CreateFunctionBody::AsReturn(expr)) } else { parser_err!("Unparsable function body", self.peek_token().span.start)? }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index c0127c6fa..c1628b165 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -303,6 +303,14 @@ fn parse_create_function() { "; 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( From 95f1bc64d146adc91895524e939f8965f98dc974 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 9 May 2025 10:57:23 -0400 Subject: [PATCH 7/8] Simplify peek/expect with `parse_keyword` --- src/parser/mod.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 77454eed1..a9a49fd2a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5232,9 +5232,7 @@ impl<'a> Parser<'a> { Some(self.parse_data_type()?) }; - if self.peek_keyword(Keyword::AS) { - self.expect_keyword_is(Keyword::AS)?; - } + let _ = self.parse_keyword(Keyword::AS); let function_body = if self.peek_keyword(Keyword::BEGIN) { let begin_token = self.expect_keyword(Keyword::BEGIN)?; @@ -5246,9 +5244,7 @@ impl<'a> Parser<'a> { statements, end_token: AttachedToken(end_token), })) - } else if self.peek_keyword(Keyword::RETURN) { - self.expect_keyword(Keyword::RETURN)?; - + } else if self.parse_keyword(Keyword::RETURN) { if self.peek_token() == Token::LParen { let expr = self.parse_expr()?; if !matches!(expr, Expr::Subquery(_)) { From 999964c4747f2da97648923c6880c35032e331d6 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 9 May 2025 11:10:01 -0400 Subject: [PATCH 8/8] Make `Table`'s ColumnDefs optional --- src/ast/data_type.rs | 14 ++++++++------ src/parser/mod.rs | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 0f1cafe7a..b731057d0 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -49,7 +49,7 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/15/sql-createfunction.html /// [MsSQL]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#c-create-a-multi-statement-table-valued-function - Table(Vec), + Table(Option>), /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). NamedTable( /// Table name. @@ -724,12 +724,14 @@ impl fmt::Display for DataType { DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), - DataType::Table(fields) => { - if fields.is_empty() { - return write!(f, "TABLE"); + DataType::Table(fields) => match fields { + Some(fields) => { + write!(f, "TABLE({})", display_comma_separated(fields)) } - write!(f, "TABLE({})", display_comma_separated(fields)) - } + None => { + write!(f, "TABLE") + } + }, DataType::NamedTable(name, fields) => { write!(f, "{} TABLE ({})", name, display_comma_separated(fields)) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a9a49fd2a..5effa1943 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5220,9 +5220,19 @@ impl<'a> Parser<'a> { p.peek_token().span.start )? }; + + if table_column_defs.is_none() + || table_column_defs.clone().is_some_and(|tcd| tcd.is_empty()) + { + parser_err!( + "Expected table column definitions after TABLE keyword", + p.peek_token().span.start + )? + } + Ok(DataType::NamedTable( ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), - table_column_defs.clone(), + table_column_defs.clone().unwrap(), )) })?; @@ -9817,10 +9827,10 @@ impl<'a> Parser<'a> { } Keyword::TABLE => { if self.peek_token() != Token::LParen { - Ok(DataType::Table(Vec::::new())) + Ok(DataType::Table(None)) } else { let columns = self.parse_returns_table_columns()?; - Ok(DataType::Table(columns)) + Ok(DataType::Table(Some(columns))) } } Keyword::SIGNED => {