diff --git a/SqlScriptDom/Parser/TSql/TSql160.g b/SqlScriptDom/Parser/TSql/TSql160.g index ed4fe5f..c45d6ff 100644 --- a/SqlScriptDom/Parser/TSql/TSql160.g +++ b/SqlScriptDom/Parser/TSql/TSql160.g @@ -31967,6 +31967,11 @@ overClause returns [OverClause vResult] )? tRParen:RightParenthesis { + // Window name inside parentheses requires at least one clause (PARTITION BY, ORDER BY, or window frame) + if (vResult.WindowName != null && vResult.Partitions.Count == 0 && vResult.OrderByClause == null && vResult.WindowFrameClause == null) + { + ThrowParseErrorException("SQL46010", tRParen, TSqlParserResource.SQL46010Message, vResult.WindowName.Value); + } UpdateTokenInfo(vResult,tRParen); } | vResult = overClauseWithWindow @@ -31989,7 +31994,7 @@ overClauseWithWindow returns [OverClause vResult = FragmentFactory.CreateFragmen overClauseNoOrderBy returns [OverClause vResult] : - vResult = overClauseBeginning + vResult = overClauseBeginningNoWindowName tRParen:RightParenthesis { UpdateTokenInfo(vResult,tRParen); @@ -32021,6 +32026,25 @@ overClauseBeginning returns [OverClause vResult = FragmentFactory.CreateFragment )? ; +overClauseBeginningNoWindowName returns [OverClause vResult = FragmentFactory.CreateFragment()] + : + tOver:Over + { + UpdateTokenInfo(vResult,tOver); + } + LeftParenthesis + ( + (Identifier By) => + tPartition:Identifier + { + Match(tPartition, CodeGenerationSupporter.Partition); + } + By expressionList[vResult, vResult.Partitions] + | + /* empty - allow OVER() with no PARTITION BY */ + ) + ; + windowFrameClause returns [WindowFrameClause vResult = FragmentFactory.CreateFragment()] : tRowsRange:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index 28673c8..65bb5a9 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -32956,6 +32956,11 @@ overClause returns [OverClause vResult] )? tRParen:RightParenthesis { + // Window name inside parentheses requires at least one clause (PARTITION BY, ORDER BY, or window frame) + if (vResult.WindowName != null && vResult.Partitions.Count == 0 && vResult.OrderByClause == null && vResult.WindowFrameClause == null) + { + ThrowParseErrorException("SQL46010", tRParen, TSqlParserResource.SQL46010Message, vResult.WindowName.Value); + } UpdateTokenInfo(vResult,tRParen); } | vResult = overClauseWithWindow @@ -32978,7 +32983,7 @@ overClauseWithWindow returns [OverClause vResult = FragmentFactory.CreateFragmen overClauseNoOrderBy returns [OverClause vResult] : - vResult = overClauseBeginning + vResult = overClauseBeginningNoWindowName tRParen:RightParenthesis { UpdateTokenInfo(vResult,tRParen); @@ -33010,6 +33015,25 @@ overClauseBeginning returns [OverClause vResult = FragmentFactory.CreateFragment )? ; +overClauseBeginningNoWindowName returns [OverClause vResult = FragmentFactory.CreateFragment()] + : + tOver:Over + { + UpdateTokenInfo(vResult,tOver); + } + LeftParenthesis + ( + (Identifier By) => + tPartition:Identifier + { + Match(tPartition, CodeGenerationSupporter.Partition); + } + By expressionList[vResult, vResult.Partitions] + | + /* empty - allow OVER() with no PARTITION BY */ + ) + ; + windowFrameClause returns [WindowFrameClause vResult = FragmentFactory.CreateFragment()] : tRowsRange:Identifier diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g index 59ed7b8..70ddca4 100644 --- a/SqlScriptDom/Parser/TSql/TSql180.g +++ b/SqlScriptDom/Parser/TSql/TSql180.g @@ -32956,6 +32956,11 @@ overClause returns [OverClause vResult] )? tRParen:RightParenthesis { + // Window name inside parentheses requires at least one clause (PARTITION BY, ORDER BY, or window frame) + if (vResult.WindowName != null && vResult.Partitions.Count == 0 && vResult.OrderByClause == null && vResult.WindowFrameClause == null) + { + ThrowParseErrorException("SQL46010", tRParen, TSqlParserResource.SQL46010Message, vResult.WindowName.Value); + } UpdateTokenInfo(vResult,tRParen); } | vResult = overClauseWithWindow @@ -32978,7 +32983,7 @@ overClauseWithWindow returns [OverClause vResult = FragmentFactory.CreateFragmen overClauseNoOrderBy returns [OverClause vResult] : - vResult = overClauseBeginning + vResult = overClauseBeginningNoWindowName tRParen:RightParenthesis { UpdateTokenInfo(vResult,tRParen); @@ -33010,6 +33015,25 @@ overClauseBeginning returns [OverClause vResult = FragmentFactory.CreateFragment )? ; +overClauseBeginningNoWindowName returns [OverClause vResult = FragmentFactory.CreateFragment()] + : + tOver:Over + { + UpdateTokenInfo(vResult,tOver); + } + LeftParenthesis + ( + (Identifier By) => + tPartition:Identifier + { + Match(tPartition, CodeGenerationSupporter.Partition); + } + By expressionList[vResult, vResult.Partitions] + | + /* empty - allow OVER() with no PARTITION BY */ + ) + ; + windowFrameClause returns [WindowFrameClause vResult = FragmentFactory.CreateFragment()] : tRowsRange:Identifier diff --git a/Test/SqlDom/Baselines160/WindowClauseTests160.sql b/Test/SqlDom/Baselines160/WindowClauseTests160.sql index 3c08bcf..20d9fee 100644 --- a/Test/SqlDom/Baselines160/WindowClauseTests160.sql +++ b/Test/SqlDom/Baselines160/WindowClauseTests160.sql @@ -103,4 +103,16 @@ SELECT Sum(t.c1) OVER Win1 FROM t1 AS t GROUP BY t.c1 WINDOW Win1 AS (PARTITION BY t.c1) -ORDER BY t.c1; \ No newline at end of file +ORDER BY t.c1; + + +GO +SELECT COUNT(*) OVER Win1 +FROM tb1 +WINDOW Win1 AS (PARTITION BY c1); + + +GO +SELECT COUNT(*) OVER (Win1 ORDER BY c1) +FROM tb1 +WINDOW Win1 AS (PARTITION BY c1); \ No newline at end of file diff --git a/Test/SqlDom/Only160SyntaxTests.cs b/Test/SqlDom/Only160SyntaxTests.cs index 966e51d..9feb868 100644 --- a/Test/SqlDom/Only160SyntaxTests.cs +++ b/Test/SqlDom/Only160SyntaxTests.cs @@ -20,7 +20,7 @@ public partial class SqlDomTests new ParserTest160("ExpressionTests160.sql", nErrors80: 1, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160("CreateUserFromExternalProvider160.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1), new ParserTest160("CreateExternalTableStatementTests160.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2), - new ParserTest160("WindowClauseTests160.sql", nErrors80: 14, nErrors90: 13, nErrors100: 13, nErrors110: 13, nErrors120: 13, nErrors130: 13, nErrors140: 13, nErrors150: 13), + new ParserTest160("WindowClauseTests160.sql", nErrors80: 16, nErrors90: 15, nErrors100: 15, nErrors110: 15, nErrors120: 15, nErrors130: 15, nErrors140: 15, nErrors150: 15), new ParserTest160("CreateExternalDataSourceStatementTests160.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160("CreateDatabaseTests160.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4), new ParserTest160("CreateLedgerTableTests160.sql", nErrors80: 14, nErrors90: 14, nErrors100: 14, nErrors110: 14, nErrors120: 14, nErrors130: 14, nErrors140: 14, nErrors150: 14), diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 5c88f7c..ce5ab88 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -2156,6 +2156,41 @@ public void IgnoreOrRecpectNullsSyntaxNegativeTest() new ParserErrorInfo(41, "SQL46010", "NULLS")); } + /// + /// Negative tests for invalid OVER clause syntax. + /// Tests for issue: Invalid OVER clause parses successfully + /// OVER clause with just a column reference in parentheses should fail. + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void InvalidOverClauseNegativeTest() + { + // OVER clause with just an identifier in parentheses is invalid + // Valid syntax is either OVER identifier (window name reference without parens) + // or OVER (PARTITION BY ...) or OVER (ORDER BY ...) or OVER () + // The error occurs at the closing paren when no clauses follow the window name + // Note: Error message shows the identifier value without quotes + // + ParserTestUtils.ErrorTest160("SELECT COUNT(*) OVER([col]) FROM t1", + new ParserErrorInfo(26, "SQL46010", "col")); + ParserTestUtils.ErrorTest170("SELECT COUNT(*) OVER([col]) FROM t1", + new ParserErrorInfo(26, "SQL46010", "col")); + ParserTestUtils.ErrorTest180("SELECT COUNT(*) OVER([col]) FROM t1", + new ParserErrorInfo(26, "SQL46010", "col")); + + // Another variant with a regular identifier + ParserTestUtils.ErrorTest160("SELECT SUM(Amount) OVER(MyColumn) FROM t1", + new ParserErrorInfo(32, "SQL46010", "MyColumn")); + ParserTestUtils.ErrorTest170("SELECT SUM(Amount) OVER(MyColumn) FROM t1", + new ParserErrorInfo(32, "SQL46010", "MyColumn")); + + // This should also fail for aggregate functions + ParserTestUtils.ErrorTest160("SELECT AVG(Value) OVER(col1) FROM t1", + new ParserErrorInfo(27, "SQL46010", "col1")); + } + + /// /// Negative tests for IS [NOT] DISTINCT FROM syntax. /// diff --git a/Test/SqlDom/TestScripts/WindowClauseTests160.sql b/Test/SqlDom/TestScripts/WindowClauseTests160.sql index 51ca862..cc0fcff 100644 --- a/Test/SqlDom/TestScripts/WindowClauseTests160.sql +++ b/Test/SqlDom/TestScripts/WindowClauseTests160.sql @@ -77,4 +77,14 @@ SELECT Sum(t.c1) OVER Win1 FROM t1 t GROUP BY t.c1 WINDOW Win1 AS (PARTITION BY t.c1) ORDER BY t.c1 +GO + +-- checking COUNT(*) with WINDOW clause and window name reference (without parentheses) +SELECT COUNT(*) OVER Win1 FROM tb1 +WINDOW Win1 AS (PARTITION BY c1) +GO + +-- checking COUNT(*) with partial window specification (window name inside parentheses) +SELECT COUNT(*) OVER (Win1 ORDER BY c1) FROM tb1 +WINDOW Win1 AS (PARTITION BY c1) GO \ No newline at end of file diff --git a/global.json b/global.json index d65b2d5..6312649 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.419", + "version": "8.0.420", "rollForward": "latestMajor" }, "msbuild-sdks": { diff --git a/release-notes/180/180.18.1.md b/release-notes/180/180.18.1.md new file mode 100644 index 0000000..345bdba --- /dev/null +++ b/release-notes/180/180.18.1.md @@ -0,0 +1,31 @@ +# Release Notes + +## Microsoft.SqlServer.TransactSql.ScriptDom 180.18.1 +This update brings the following changes over the previous release: + +### Target Platform Support + +* .NET Framework 4.7.2 (Windows x86, Windows x64) +* .NET 8 (Windows x86, Windows x64, Linux, macOS) +* .NET Standard 2.0+ (Windows x86, Windows x64, Linux, macOS) + +### Dependencies +* Updates .NET SDK to latest patch version 8.0.420 + +#### .NET Framework +#### .NET Core + +### New Features + +### Fixed +* TRIM with FROM clause in RETURN statement fails to parse [#188](https://github.com/microsoft/SqlScriptDOM/issues/188) +* DATEADD/DATEDIFF/DATEPART/DATENAME first argument parsed as ColumnReferenceExpression instead of datepart keyword [#196](https://github.com/microsoft/SqlScriptDOM/issues/196) +* DAY used as argument for DATEADD() is interpreted as ColumnReferenceExpression [#124](https://github.com/microsoft/SqlScriptDOM/issues/124) +* TSql160Parser misidentifies interval parameter in DATEDIFF function as ColumnReferenceExpression [#98](https://github.com/microsoft/SqlScriptDOM/issues/98) +* Invalid OVER clause parses successfully [#195](https://github.com/microsoft/SqlScriptDOM/issues/195) + +### Changes +* Updates the Transact-SQL script generator to ensure that semicolons are correctly placed before any trailing comments + +### Known Issues +* None \ No newline at end of file