diff --git a/.gitignore b/.gitignore index c606ba9..997c172 100644 --- a/.gitignore +++ b/.gitignore @@ -356,4 +356,7 @@ MigrationBackup/ out/ # Project packages folder -.packages/ \ No newline at end of file +.packages/ + +# Temporary build artifacts +tmp/ \ No newline at end of file diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml index ffeec7c..e5764b2 100644 --- a/SqlScriptDom/Parser/TSql/Ast.xml +++ b/SqlScriptDom/Parser/TSql/Ast.xml @@ -4600,6 +4600,13 @@ + + + + + + + diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index b5b4a2b..031bc61 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -883,6 +883,9 @@ create2005Statements returns [TSqlStatement vResult = null] | {NextTokenMatches(CodeGenerationSupporter.ColumnStore)}? vResult=createColumnStoreIndexStatement[null, null] + | + {NextTokenMatches(CodeGenerationSupporter.Json)}? + vResult=createJsonIndexStatement[null, null] | {NextTokenMatches(CodeGenerationSupporter.Contract)}? vResult=createContractStatement @@ -16844,6 +16847,7 @@ createIndexStatement returns [TSqlStatement vResult = null] ( vResult=createRelationalIndexStatement[tUnique, isClustered] | vResult=createColumnStoreIndexStatement[tUnique, isClustered] + | vResult=createJsonIndexStatement[tUnique, isClustered] ) ) | @@ -16980,6 +16984,58 @@ createColumnStoreIndexStatement [IToken tUnique, bool? isClustered] returns [Cre )? ; +createJsonIndexStatement [IToken tUnique, bool? isClustered] returns [CreateJsonIndexStatement vResult = FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; + SchemaObjectName vSchemaObjectName; + Identifier vJsonColumn; + StringLiteral vPath; + + if (tUnique != null) + { + ThrowIncorrectSyntaxErrorException(tUnique); + } + if (isClustered.HasValue) + { + ThrowIncorrectSyntaxErrorException(LT(1)); + } +} + : tJson:Identifier tIndex:Index vIdentifier=identifier + { + Match(tJson, CodeGenerationSupporter.Json); + vResult.Name = vIdentifier; + } + tOn:On vSchemaObjectName=schemaObjectThreePartName + { + vResult.OnName = vSchemaObjectName; + } + LeftParenthesis vJsonColumn=identifier tRParen:RightParenthesis + { + vResult.JsonColumn = vJsonColumn; + UpdateTokenInfo(vResult, tRParen); + } + ( + tFor:For LeftParenthesis + vPath=stringLiteral + { + AddAndUpdateTokenInfo(vResult, vResult.ForJsonPaths, vPath); + } + ( + Comma vPath=stringLiteral + { + AddAndUpdateTokenInfo(vResult, vResult.ForJsonPaths, vPath); + } + )* + RightParenthesis + )? + ( + // Greedy due to conflict with withCommonTableExpressionsAndXmlNamespaces + options {greedy = true; } : + With + indexOptionList[IndexAffectingStatement.CreateIndex, vResult.IndexOptions, vResult] + )? + ; + indexKeyColumnList[CreateIndexStatement vParent] { ColumnWithSortOrder vColumnWithSortOrder; diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateJsonIndexStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateJsonIndexStatement.cs new file mode 100644 index 0000000..e7665dd --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateJsonIndexStatement.cs @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using System.Collections.Generic; +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + public override void ExplicitVisit(CreateJsonIndexStatement node) + { + GenerateKeyword(TSqlTokenType.Create); + + GenerateSpaceAndIdentifier(CodeGenerationSupporter.Json); + + GenerateSpaceAndKeyword(TSqlTokenType.Index); + + // name + GenerateSpaceAndFragmentIfNotNull(node.Name); + + NewLineAndIndent(); + GenerateKeyword(TSqlTokenType.On); + GenerateSpaceAndFragmentIfNotNull(node.OnName); + + // JSON column + if (node.JsonColumn != null) + { + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateFragmentIfNotNull(node.JsonColumn); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + + // FOR clause with JSON paths + if (node.ForJsonPaths != null && node.ForJsonPaths.Count > 0) + { + NewLineAndIndent(); + GenerateKeyword(TSqlTokenType.For); + GenerateSpace(); + GenerateParenthesisedCommaSeparatedList(node.ForJsonPaths); + } + + GenerateIndexOptions(node.IndexOptions); + } + } +} \ No newline at end of file diff --git a/Test/SqlDom/Baselines170/JsonIndexTests170.sql b/Test/SqlDom/Baselines170/JsonIndexTests170.sql new file mode 100644 index 0000000..f6010de --- /dev/null +++ b/Test/SqlDom/Baselines170/JsonIndexTests170.sql @@ -0,0 +1,29 @@ +CREATE JSON INDEX IX_JSON_Basic + ON dbo.Users (JsonData); + +CREATE JSON INDEX IX_JSON_SinglePath + ON dbo.Users (JsonData) + FOR ('$.name'); + +CREATE JSON INDEX IX_JSON_MultiplePaths + ON dbo.Users (JsonData) + FOR ('$.name', '$.email', '$.age'); + +CREATE JSON INDEX IX_JSON_WithOptions + ON dbo.Users (JsonData) WITH (FILLFACTOR = 90, ONLINE = OFF); + +CREATE JSON INDEX IX_JSON_Complete + ON dbo.Users (JsonData) + FOR ('$.profile.name', '$.profile.email') WITH (MAXDOP = 4, DATA_COMPRESSION = ROW); + +CREATE JSON INDEX IX_JSON_Schema + ON MySchema.MyTable (JsonColumn) + FOR ('$.properties.value'); + +CREATE JSON INDEX [IX JSON Index] + ON [dbo].[Users] ([Json Data]) + FOR ('$.data.attributes'); + +CREATE JSON INDEX IX_JSON_Complex + ON dbo.Documents (Content) + FOR ('$.metadata.title', '$.content.sections[*].text', '$.tags[*]'); \ No newline at end of file diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index bd84d40..654fcdb 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -10,6 +10,7 @@ public partial class SqlDomTests private static readonly ParserTest[] Only170TestInfos = { new ParserTest170("RegexpTVFTests170.sql", nErrors80: 1, nErrors90: 1, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0), + new ParserTest170("JsonIndexTests170.sql", nErrors80: 2, nErrors90: 8, nErrors100: 8, nErrors110: 8, nErrors120: 8, nErrors130: 8, nErrors140: 8, nErrors150: 8, nErrors160: 8), new ParserTest170("AlterDatabaseManualCutoverTests170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4), new ParserTest170("CreateColumnStoreIndexTests170.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 3, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0) }; diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 57f6264..f7632b5 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -4384,6 +4384,58 @@ public void CreateIndexStatementErrorTest() new ParserErrorInfo(47, "SQL46010", "col1")); } + /// + /// JSON Index error tests - ensure JSON Index syntax is rejected in older versions and malformed syntax produces appropriate errors + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void CreateJsonIndexStatementErrorTest() + { + // JSON Index syntax should not be supported in SQL Server versions prior to 2025 (TSql170) + // Test basic JSON Index syntax in older versions + ParserTestUtils.ErrorTest160("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest150("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest140("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest130("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest120("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest110("CREATE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(7, "SQL46010", "JSON")); + + + + // Test that UNIQUE and CLUSTERED/NONCLUSTERED are not allowed with JSON indexes in TSql170 + TSql170Parser parser170 = new TSql170Parser(true); + ParserTestUtils.ErrorTest(parser170, "CREATE UNIQUE JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(14, "SQL46010", "JSON")); + ParserTestUtils.ErrorTest(parser170, "CREATE CLUSTERED JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(17, "SQL46005", "COLUMNSTORE", "JSON")); + ParserTestUtils.ErrorTest(parser170, "CREATE NONCLUSTERED JSON INDEX idx1 ON table1 (jsonColumn)", + new ParserErrorInfo(20, "SQL46005", "COLUMNSTORE", "JSON")); + + // Test malformed JSON Index syntax in TSql170 + // Missing column specification + ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1", + new ParserErrorInfo(32, "SQL46029")); + + // Empty FOR clause + ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1 (jsonColumn) FOR ()", + new ParserErrorInfo(51, "SQL46010", ")")); + + // Invalid JSON path (missing quotes) + ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1 (jsonColumn) FOR ($.name)", + new ParserErrorInfo(51, "SQL46010", "$")); + + // Missing table name + ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON (jsonColumn)", + new ParserErrorInfo(26, "SQL46010", "(")); + } + /// /// Check that the value of MAXDOP index option is within range /// diff --git a/Test/SqlDom/TestScripts/JsonIndexTests170.sql b/Test/SqlDom/TestScripts/JsonIndexTests170.sql new file mode 100644 index 0000000..2a0f1ad --- /dev/null +++ b/Test/SqlDom/TestScripts/JsonIndexTests170.sql @@ -0,0 +1,31 @@ +-- Basic JSON index creation +CREATE JSON INDEX IX_JSON_Basic ON dbo.Users (JsonData); + +-- JSON index with FOR clause (single path) +CREATE JSON INDEX IX_JSON_SinglePath ON dbo.Users (JsonData) +FOR ('$.name'); + +-- JSON index with FOR clause (multiple paths) +CREATE JSON INDEX IX_JSON_MultiplePaths ON dbo.Users (JsonData) +FOR ('$.name', '$.email', '$.age'); + +-- JSON index with WITH options +CREATE JSON INDEX IX_JSON_WithOptions ON dbo.Users (JsonData) +WITH (FILLFACTOR = 90, ONLINE = OFF); + +-- JSON index with FOR clause and WITH options +CREATE JSON INDEX IX_JSON_Complete ON dbo.Users (JsonData) +FOR ('$.profile.name', '$.profile.email') +WITH (MAXDOP = 4, DATA_COMPRESSION = ROW); + +-- JSON index on schema-qualified table +CREATE JSON INDEX IX_JSON_Schema ON MySchema.MyTable (JsonColumn) +FOR ('$.properties.value'); + +-- JSON index with quoted identifiers +CREATE JSON INDEX [IX JSON Index] ON [dbo].[Users] ([Json Data]) +FOR ('$.data.attributes'); + +-- JSON index with complex path expressions +CREATE JSON INDEX IX_JSON_Complex ON dbo.Documents (Content) +FOR ('$.metadata.title', '$.content.sections[*].text', '$.tags[*]'); \ No newline at end of file diff --git a/global.json b/global.json index 0e56f42..457d429 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.411", + "version": "8.0.117", "rollForward": "latestMajor" }, "msbuild-sdks": {