From e2c8b8b7d1f78f3b182484a7426c362a640c6823 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 14:18:15 -0400 Subject: [PATCH 01/16] 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 270897130..7ddbf5e5a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2346,6 +2346,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 d711a1062..2ef12915a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8778,6 +8778,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 992d19c49..11aa60b01 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5208,15 +5208,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, @@ -9784,8 +9795,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 7b3769ec8..7f8e4baf1 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 028817760bfa90b6f0f3d424a149a0cff77904bb Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 15:12:21 -0400 Subject: [PATCH 02/16] 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 11aa60b01..84362f0d1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5204,7 +5204,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 7f8e4baf1..04dc50737 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 25a4416e85923da4385bde29ed53721a3bc8ff54 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 16:07:05 -0400 Subject: [PATCH 03/16] 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 84362f0d1..823e241be 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5230,7 +5230,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)?; @@ -9819,7 +9821,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 04dc50737..377bcbc18 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 b84ec500814c1445ac9ffae03437084d59880a7c Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 16:39:37 -0400 Subject: [PATCH 04/16] 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 823e241be..eb2c874a2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9867,13 +9867,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 377bcbc18..221529cdc 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 c2fd171a98580cf2c8f844fbaab2c1a9693a9a1c Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 6 May 2025 19:38:52 -0400 Subject: [PATCH 05/16] 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 eb2c874a2..27adc6ee6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5216,7 +5216,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 9e45f27d5831138053c5b3f9cbc3b4e69eebff8f Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 7 May 2025 16:39:32 -0400 Subject: [PATCH 06/16] 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 7ddbf5e5a..87d11b7d3 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2346,7 +2346,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 2ef12915a..5856c2afc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8789,7 +8789,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 27adc6ee6..b269e4855 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5249,11 +5249,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 221529cdc..534d24681 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 10919b5a117304c495df1da29cb8ef476138fb37 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 9 May 2025 10:57:23 -0400 Subject: [PATCH 07/16] 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 b269e4855..08e91f705 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5233,9 +5233,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)?; @@ -5247,9 +5245,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 6798bcc18b0d1c931507b07c910a26f63b941dd4 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 9 May 2025 11:10:01 -0400 Subject: [PATCH 08/16] 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 08e91f705..425ffda6b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5221,9 +5221,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(), )) })?; @@ -9835,10 +9845,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 => { From 846e5c91c535b047f408aa7440c0bafd3a944aa3 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 13 May 2025 12:17:32 -0400 Subject: [PATCH 09/16] Refactor to avoid cloning/unwrapping --- src/parser/mod.rs | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 425ffda6b..d7f997019 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5207,33 +5207,40 @@ impl<'a> Parser<'a> { 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 { + + if !p.peek_keyword(Keyword::TABLE) { parser_err!( "Expected TABLE keyword after return type", 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", + let table_column_defs = match p.parse_data_type()? { + DataType::Table(maybe_table_column_defs) => match maybe_table_column_defs { + Some(table_column_defs) => { + if table_column_defs.is_empty() { + parser_err!( + "Expected table column definitions after TABLE keyword", + p.peek_token().span.start + )? + } + + table_column_defs + } + None => parser_err!( + "Expected table column definitions after TABLE keyword", + p.peek_token().span.start + )?, + }, + _ => parser_err!( + "Expected table data type after TABLE keyword", p.peek_token().span.start - )? - } + )?, + }; Ok(DataType::NamedTable( ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), - table_column_defs.clone().unwrap(), + table_column_defs, )) })?; From e36933c2ac67fcde3df71a828ac96cacf2e2f329 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 13 May 2025 12:21:40 -0400 Subject: [PATCH 10/16] Refactor NamedTable to be a regular struct --- src/ast/data_type.rs | 12 ++++++------ src/parser/mod.rs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index b731057d0..1c9dfba6c 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -51,12 +51,12 @@ pub enum DataType { /// [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(Option>), /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). - NamedTable( + NamedTable { /// Table name. - ObjectName, + name: ObjectName, /// Table columns. - Vec, - ), + columns: Vec, + }, /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -732,8 +732,8 @@ impl fmt::Display for DataType { write!(f, "TABLE") } }, - DataType::NamedTable(name, fields) => { - write!(f, "{} TABLE ({})", name, display_comma_separated(fields)) + DataType::NamedTable { name, columns } => { + write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } DataType::GeometricType(kind) => write!(f, "{}", kind), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d7f997019..ec93d4869 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5238,10 +5238,10 @@ impl<'a> Parser<'a> { )?, }; - Ok(DataType::NamedTable( - ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), - table_column_defs, - )) + Ok(DataType::NamedTable { + name: ObjectName(vec![ObjectNamePart::Identifier(return_table_name)]), + columns: table_column_defs, + }) })?; let return_type = if return_table.is_some() { From 0e11006b235da33b26482ff91023cea42148899c Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 13 May 2025 12:23:59 -0400 Subject: [PATCH 11/16] Add documentation link for named tables types --- src/ast/data_type.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 1c9dfba6c..3a4958c9f 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -51,6 +51,8 @@ pub enum DataType { /// [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(Option>), /// Table type with a name, e.g. CREATE FUNCTION RETURNS @result TABLE(...). + /// + /// [MsSQl]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver16#table NamedTable { /// Table name. name: ObjectName, From 4dd05d13c4c6577f9b6d7686d925bb8023d6d5f0 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 13 May 2025 12:25:42 -0400 Subject: [PATCH 12/16] Add documentation link for RETURN SELECT --- src/ast/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5856c2afc..c9f0b3e9f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8799,6 +8799,8 @@ pub enum CreateFunctionBody { /// 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), } From 486f85ae78870a683a45c3727dde6f95754ba49e Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 15 May 2025 13:26:02 -0400 Subject: [PATCH 13/16] Add comment to explain LParen behavior - plus, flip the if/else to positive equality for simplicity --- src/parser/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ec93d4869..cc42f520b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9851,11 +9851,13 @@ impl<'a> Parser<'a> { Ok(DataType::AnyType) } Keyword::TABLE => { - if self.peek_token() != Token::LParen { - Ok(DataType::Table(None)) - } else { + // 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 => { From 8bc8d386a270a3b16cf226660f6ee2a415939454 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Thu, 15 May 2025 13:52:52 -0400 Subject: [PATCH 14/16] Rename `AsReturnSubquery` to `AsReturnExpr` & remove non-subquery error --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 2 +- src/parser/mod.rs | 9 +-------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 87d11b7d3..6275a4201 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2346,7 +2346,7 @@ impl fmt::Display for CreateFunction { if let Some(CreateFunctionBody::Return(function_body)) = &self.function_body { write!(f, " RETURN {function_body}")?; } - if let Some(CreateFunctionBody::AsReturnSubquery(function_body)) = &self.function_body { + if let Some(CreateFunctionBody::AsReturnExpr(function_body)) = &self.function_body { write!(f, " AS RETURN {function_body}")?; } if let Some(CreateFunctionBody::AsReturnSelect(function_body)) = &self.function_body { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c9f0b3e9f..cb297af63 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8789,7 +8789,7 @@ pub enum CreateFunctionBody { /// ``` /// /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql - AsReturnSubquery(Expr), + AsReturnExpr(Expr), /// Function body expression using the 'AS RETURN' keywords, with an un-parenthesized SELECT query /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cc42f520b..6a6de2256 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5264,14 +5264,7 @@ impl<'a> Parser<'a> { })) } else if self.parse_keyword(Keyword::RETURN) { 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)) + Some(CreateFunctionBody::AsReturnExpr(self.parse_expr()?)) } else if self.peek_keyword(Keyword::SELECT) { let select = self.parse_select()?; Some(CreateFunctionBody::AsReturnSelect(select)) From 9b6d6b1caeb4c4321b6fca7d5a3a12d5abd728fc Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 20 May 2025 10:05:22 -0400 Subject: [PATCH 15/16] Switch from peek to expect + prev --- 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 6a6de2256..a2bf24e9b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5208,12 +5208,8 @@ impl<'a> Parser<'a> { let return_table = self.maybe_parse(|p| { let return_table_name = p.parse_identifier()?; - if !p.peek_keyword(Keyword::TABLE) { - parser_err!( - "Expected TABLE keyword after return type", - p.peek_token().span.start - )? - } + p.expect_keyword_is(Keyword::TABLE)?; + p.prev_token(); let table_column_defs = match p.parse_data_type()? { DataType::Table(maybe_table_column_defs) => match maybe_table_column_defs { From 0add6e250c468d97519f1ab46a92711e084beb19 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 20 May 2025 10:20:06 -0400 Subject: [PATCH 16/16] Simplify TABLE type parser errors & add test examples for parse failures --- src/parser/mod.rs | 21 ++++----------------- tests/sqlparser_mssql.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2bf24e9b..e10d77aeb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5212,24 +5212,11 @@ impl<'a> Parser<'a> { p.prev_token(); let table_column_defs = match p.parse_data_type()? { - DataType::Table(maybe_table_column_defs) => match maybe_table_column_defs { - Some(table_column_defs) => { - if table_column_defs.is_empty() { - parser_err!( - "Expected table column definitions after TABLE keyword", - p.peek_token().span.start - )? - } - - table_column_defs - } - None => parser_err!( - "Expected table column definitions after TABLE keyword", - p.peek_token().span.start - )?, - }, + DataType::Table(Some(table_column_defs)) if !table_column_defs.is_empty() => { + table_column_defs + } _ => parser_err!( - "Expected table data type after TABLE keyword", + "Expected table column definitions after TABLE keyword", p.peek_token().span.start )?, }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 534d24681..32388c44a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -346,6 +346,35 @@ fn parse_create_function() { 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]